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
|
- libgconf-2-4
|
||||||
snaps:
|
snaps:
|
||||||
- docker
|
- docker
|
||||||
|
firefox: "latest-esr"
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- yarn global add wait-on
|
- yarn global add wait-on
|
||||||
# Install Codecov
|
# 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).
|
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)
|
#### [v0.3.1](https://github.com/Human-Connection/Human-Connection/compare/v0.3.0...v0.3.1)
|
||||||
|
|
||||||
> 10 February 2020
|
> 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)
|
- 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)
|
- 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)
|
- 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)
|
- refactor TeaserImage component [`e14cbf8`](https://github.com/Human-Connection/Human-Connection/commit/e14cbf8173e3040b5285ba6a5c73e2d2d2a47860)
|
||||||
- Update to v0.3.0 [`dbe2c4c`](https://github.com/Human-Connection/Human-Connection/commit/dbe2c4cdd5bab2195c6369b84989507b9f7da768)
|
- 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)
|
#### [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)
|
- 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)
|
- 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)
|
- 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)
|
#### [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",
|
"name": "human-connection-backend",
|
||||||
"version": "0.3.1",
|
"version": "0.4.0",
|
||||||
"description": "GraphQL Backend for Human Connection",
|
"description": "GraphQL Backend for Human Connection",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -38,19 +38,19 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hapi/joi": "^17.1.0",
|
"@hapi/joi": "^17.1.0",
|
||||||
"@sentry/node": "^5.12.3",
|
"@sentry/node": "^5.13.1",
|
||||||
"apollo-cache-inmemory": "~1.6.5",
|
"apollo-cache-inmemory": "~1.6.5",
|
||||||
"apollo-client": "~2.6.8",
|
"apollo-client": "~2.6.8",
|
||||||
"apollo-link-context": "~1.0.19",
|
"apollo-link-context": "~1.0.19",
|
||||||
"apollo-link-http": "~1.5.16",
|
"apollo-link-http": "~1.5.16",
|
||||||
"apollo-server": "~2.10.0",
|
"apollo-server": "~2.10.1",
|
||||||
"apollo-server-express": "^2.9.16",
|
"apollo-server-express": "^2.9.16",
|
||||||
"babel-plugin-transform-runtime": "^6.23.0",
|
"babel-plugin-transform-runtime": "^6.23.0",
|
||||||
"bcryptjs": "~2.4.3",
|
"bcryptjs": "~2.4.3",
|
||||||
"cheerio": "~1.0.0-rc.3",
|
"cheerio": "~1.0.0-rc.3",
|
||||||
"cors": "~2.8.5",
|
"cors": "~2.8.5",
|
||||||
"cross-env": "~7.0.0",
|
"cross-env": "~7.0.0",
|
||||||
"date-fns": "2.9.0",
|
"date-fns": "2.10.0",
|
||||||
"debug": "~4.1.1",
|
"debug": "~4.1.1",
|
||||||
"dotenv": "~8.2.0",
|
"dotenv": "~8.2.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
@ -61,30 +61,30 @@
|
|||||||
"graphql-middleware": "~4.0.2",
|
"graphql-middleware": "~4.0.2",
|
||||||
"graphql-middleware-sentry": "^3.2.1",
|
"graphql-middleware-sentry": "^3.2.1",
|
||||||
"graphql-redis-subscriptions": "^2.1.2",
|
"graphql-redis-subscriptions": "^2.1.2",
|
||||||
"graphql-shield": "~7.0.11",
|
"graphql-shield": "~7.0.14",
|
||||||
"graphql-tag": "~2.10.3",
|
"graphql-tag": "~2.10.3",
|
||||||
"helmet": "~3.21.2",
|
"helmet": "~3.21.3",
|
||||||
"ioredis": "^4.14.1",
|
"ioredis": "^4.16.0",
|
||||||
"jsonwebtoken": "~8.5.1",
|
"jsonwebtoken": "~8.5.1",
|
||||||
"linkifyjs": "~2.1.8",
|
"linkifyjs": "~2.1.8",
|
||||||
"lodash": "~4.17.14",
|
"lodash": "~4.17.14",
|
||||||
"merge-graphql-schemas": "^1.7.6",
|
"merge-graphql-schemas": "^1.7.6",
|
||||||
"metascraper": "^5.11.0",
|
"metascraper": "^5.11.4",
|
||||||
"metascraper-audio": "^5.11.1",
|
"metascraper-audio": "^5.11.1",
|
||||||
"metascraper-author": "^5.10.7",
|
"metascraper-author": "^5.11.1",
|
||||||
"metascraper-clearbit-logo": "^5.3.0",
|
"metascraper-clearbit-logo": "^5.3.0",
|
||||||
"metascraper-date": "^5.10.7",
|
"metascraper-date": "^5.11.1",
|
||||||
"metascraper-description": "^5.11.0",
|
"metascraper-description": "^5.11.1",
|
||||||
"metascraper-image": "^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-lang-detector": "^4.10.2",
|
||||||
"metascraper-logo": "^5.10.7",
|
"metascraper-logo": "^5.11.1",
|
||||||
"metascraper-publisher": "^5.10.7",
|
"metascraper-publisher": "^5.11.1",
|
||||||
"metascraper-soundcloud": "^5.10.7",
|
"metascraper-soundcloud": "^5.11.4",
|
||||||
"metascraper-title": "^5.10.7",
|
"metascraper-title": "^5.11.1",
|
||||||
"metascraper-url": "^5.10.7",
|
"metascraper-url": "^5.11.1",
|
||||||
"metascraper-video": "^5.10.7",
|
"metascraper-video": "^5.11.1",
|
||||||
"metascraper-youtube": "^5.10.7",
|
"metascraper-youtube": "^5.11.1",
|
||||||
"migrate": "^1.6.2",
|
"migrate": "^1.6.2",
|
||||||
"minimatch": "^3.0.4",
|
"minimatch": "^3.0.4",
|
||||||
"mustache": "^4.0.0",
|
"mustache": "^4.0.0",
|
||||||
@ -92,29 +92,29 @@
|
|||||||
"neo4j-graphql-js": "^2.11.5",
|
"neo4j-graphql-js": "^2.11.5",
|
||||||
"neode": "^0.3.7",
|
"neode": "^0.3.7",
|
||||||
"node-fetch": "~2.6.0",
|
"node-fetch": "~2.6.0",
|
||||||
"nodemailer": "^6.4.2",
|
"nodemailer": "^6.4.4",
|
||||||
"nodemailer-html-to-text": "^3.1.0",
|
"nodemailer-html-to-text": "^3.1.0",
|
||||||
"npm-run-all": "~4.1.5",
|
"npm-run-all": "~4.1.5",
|
||||||
"request": "~2.88.2",
|
"request": "~2.88.2",
|
||||||
"sanitize-html": "~1.21.1",
|
"sanitize-html": "~1.22.0",
|
||||||
"slug": "~2.1.1",
|
"slug": "~2.1.1",
|
||||||
"subscriptions-transport-ws": "^0.9.16",
|
"subscriptions-transport-ws": "^0.9.16",
|
||||||
"trunc-html": "~1.1.2",
|
"trunc-html": "~1.1.2",
|
||||||
"uuid": "~3.4.0",
|
"uuid": "~7.0.1",
|
||||||
"validator": "^12.2.0",
|
"validator": "^12.2.0",
|
||||||
"wait-on": "~4.0.0",
|
"wait-on": "~4.0.1",
|
||||||
"xregexp": "^4.2.4"
|
"xregexp": "^4.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "~7.8.4",
|
"@babel/cli": "~7.8.4",
|
||||||
"@babel/core": "~7.8.4",
|
"@babel/core": "~7.8.6",
|
||||||
"@babel/node": "~7.8.4",
|
"@babel/node": "~7.8.4",
|
||||||
"@babel/plugin-proposal-throw-expressions": "^7.8.3",
|
"@babel/plugin-proposal-throw-expressions": "^7.8.3",
|
||||||
"@babel/preset-env": "~7.8.4",
|
"@babel/preset-env": "~7.8.6",
|
||||||
"@babel/register": "^7.8.3",
|
"@babel/register": "^7.8.6",
|
||||||
"apollo-server-testing": "~2.10.1",
|
"apollo-server-testing": "~2.10.1",
|
||||||
"babel-core": "~7.0.0-0",
|
"babel-core": "~7.0.0-0",
|
||||||
"babel-eslint": "~10.0.3",
|
"babel-eslint": "~10.1.0",
|
||||||
"babel-jest": "~25.1.0",
|
"babel-jest": "~25.1.0",
|
||||||
"chai": "~4.2.0",
|
"chai": "~4.2.0",
|
||||||
"cucumber": "~6.0.5",
|
"cucumber": "~6.0.5",
|
||||||
@ -122,7 +122,7 @@
|
|||||||
"eslint-config-prettier": "~6.10.0",
|
"eslint-config-prettier": "~6.10.0",
|
||||||
"eslint-config-standard": "~14.1.0",
|
"eslint-config-standard": "~14.1.0",
|
||||||
"eslint-plugin-import": "~2.20.1",
|
"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-node": "~11.0.0",
|
||||||
"eslint-plugin-prettier": "~3.1.2",
|
"eslint-plugin-prettier": "~3.1.2",
|
||||||
"eslint-plugin-promise": "~4.2.1",
|
"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 faker from 'faker'
|
||||||
import slugify from 'slug'
|
import slugify from 'slug'
|
||||||
import { hashSync } from 'bcryptjs'
|
import { hashSync } from 'bcryptjs'
|
||||||
|
|||||||
@ -152,6 +152,7 @@ export default shield(
|
|||||||
User: {
|
User: {
|
||||||
email: or(isMyOwn, isAdmin),
|
email: or(isMyOwn, isAdmin),
|
||||||
},
|
},
|
||||||
|
Report: isModerator,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
debug,
|
debug,
|
||||||
|
|||||||
@ -9,14 +9,6 @@ const driver = getDriver()
|
|||||||
|
|
||||||
let query, authenticatedUser, owner, anotherRegularUser, administrator, variables, moderator
|
let query, authenticatedUser, owner, anotherRegularUser, administrator, variables, moderator
|
||||||
|
|
||||||
const userQuery = gql`
|
|
||||||
query($name: String) {
|
|
||||||
User(name: $name) {
|
|
||||||
email
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
describe('authorization', () => {
|
describe('authorization', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await cleanDatabase()
|
await cleanDatabase()
|
||||||
@ -30,7 +22,11 @@ describe('authorization', () => {
|
|||||||
query = createTestClient(server).query
|
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 () => {
|
beforeEach(async () => {
|
||||||
;[owner, anotherRegularUser, administrator, moderator] = await Promise.all([
|
;[owner, anotherRegularUser, administrator, moderator] = await Promise.all([
|
||||||
Factory.build(
|
Factory.build(
|
||||||
@ -79,15 +75,20 @@ describe('authorization', () => {
|
|||||||
variables = {}
|
variables = {}
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await cleanDatabase()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('access email address', () => {
|
describe('access email address', () => {
|
||||||
|
const userQuery = gql`
|
||||||
|
query($name: String) {
|
||||||
|
User(name: $name) {
|
||||||
|
email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
describe('unauthenticated', () => {
|
describe('unauthenticated', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
authenticatedUser = null
|
authenticatedUser = null
|
||||||
})
|
})
|
||||||
|
|
||||||
it("throws an error and does not expose the owner's email address", async () => {
|
it("throws an error and does not expose the owner's email address", async () => {
|
||||||
await expect(
|
await expect(
|
||||||
query({ query: userQuery, variables: { name: 'Owner' } }),
|
query({ query: userQuery, variables: { name: 'Owner' } }),
|
||||||
@ -143,7 +144,7 @@ describe('authorization', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('administrator', () => {
|
describe('as an administrator', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
authenticatedUser = await administrator.toJson()
|
authenticatedUser = await administrator.toJson()
|
||||||
})
|
})
|
||||||
|
|||||||
@ -153,7 +153,7 @@ describe('slugifyMiddleware', () => {
|
|||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
Learn how to setup the database here:
|
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
|
reasonCategory: $reasonCategory
|
||||||
reasonDescription: $reasonDescription
|
reasonDescription: $reasonDescription
|
||||||
) {
|
) {
|
||||||
id
|
reportId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import uuid from 'uuid/v4'
|
import { v4 as uuid } from 'uuid'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
id: { type: 'string', primary: true, default: uuid },
|
id: { type: 'string', primary: true, default: uuid },
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import uuid from 'uuid/v4'
|
import { v4 as uuid } from 'uuid'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
id: { type: 'string', primary: true, default: uuid },
|
id: { type: 'string', primary: true, default: uuid },
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import uuid from 'uuid/v4'
|
import { v4 as uuid } from 'uuid'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
id: { type: 'string', primary: true, default: uuid },
|
id: { type: 'string', primary: true, default: uuid },
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import uuid from 'uuid/v4'
|
import { v4 as uuid } from 'uuid'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
id: { type: 'string', primary: true, default: uuid },
|
id: { type: 'string', primary: true, default: uuid },
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import uuid from 'uuid/v4'
|
import { v4 as uuid } from 'uuid'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
id: { type: 'string', primary: true, default: uuid },
|
id: { type: 'string', primary: true, default: uuid },
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import uuid from 'uuid/v4'
|
import { v4 as uuid } from 'uuid'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
id: { type: 'string', primary: true, default: uuid },
|
id: { type: 'string', primary: true, default: uuid },
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import uuid from 'uuid/v4'
|
import { v4 as uuid } from 'uuid'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
id: { type: 'string', primary: true, default: uuid }, // TODO: should be type: 'uuid' but simplified for our tests
|
id: { type: 'string', primary: true, default: uuid }, // TODO: should be type: 'uuid' but simplified for our tests
|
||||||
|
|||||||
@ -46,7 +46,7 @@ describe('slug', () => {
|
|||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
Learn how to setup the database here:
|
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'
|
import Resolver from './helpers/Resolver'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { createWriteStream } from 'fs'
|
import { createWriteStream } from 'fs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import slug from 'slug'
|
import slug from 'slug'
|
||||||
import uuid from 'uuid/v4'
|
import { v4 as uuid } from 'uuid'
|
||||||
|
|
||||||
const localFileUpload = async ({ createReadStream, uniqueFilename }) => {
|
const localFileUpload = async ({ createReadStream, uniqueFilename }) => {
|
||||||
await new Promise((resolve, reject) =>
|
await new Promise((resolve, reject) =>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import uuid from 'uuid/v4'
|
import { v4 as uuid } from 'uuid'
|
||||||
export default function generateNonce() {
|
export default function generateNonce() {
|
||||||
return uuid().substring(0, 6)
|
return uuid().substring(0, 6)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,20 +1,10 @@
|
|||||||
const transformReturnType = record => {
|
import log from './helpers/databaseLogger'
|
||||||
return {
|
|
||||||
...record.get('review').properties,
|
|
||||||
report: record.get('report').properties,
|
|
||||||
resource: {
|
|
||||||
__typename: record.get('type'),
|
|
||||||
...record.get('resource').properties,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Mutation: {
|
Mutation: {
|
||||||
review: async (_object, params, context, _resolveInfo) => {
|
review: async (_object, params, context, _resolveInfo) => {
|
||||||
const { user: moderator, driver } = context
|
const { user: moderator, driver } = context
|
||||||
|
|
||||||
let createdRelationshipWithNestedAttributes = null // return value
|
|
||||||
const session = driver.session()
|
const session = driver.session()
|
||||||
try {
|
try {
|
||||||
const cypher = `
|
const cypher = `
|
||||||
@ -25,10 +15,11 @@ export default {
|
|||||||
ON CREATE SET review.createdAt = $dateTime, review.updatedAt = review.createdAt
|
ON CREATE SET review.createdAt = $dateTime, review.updatedAt = review.createdAt
|
||||||
ON MATCH SET review.updatedAt = $dateTime
|
ON MATCH SET review.updatedAt = $dateTime
|
||||||
SET review.disable = $params.disable
|
SET review.disable = $params.disable
|
||||||
SET report.updatedAt = $dateTime, report.closed = $params.closed
|
SET report.updatedAt = $dateTime, report.disable = review.disable, report.closed = $params.closed
|
||||||
SET resource.disabled = review.disable
|
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 reviewWriteTxResultPromise = session.writeTransaction(async txc => {
|
||||||
const reviewTransactionResponse = await txc.run(cypher, {
|
const reviewTransactionResponse = await txc.run(cypher, {
|
||||||
@ -36,16 +27,14 @@ export default {
|
|||||||
moderatorId: moderator.id,
|
moderatorId: moderator.id,
|
||||||
dateTime: new Date().toISOString(),
|
dateTime: new Date().toISOString(),
|
||||||
})
|
})
|
||||||
return reviewTransactionResponse.records.map(transformReturnType)
|
log(reviewTransactionResponse)
|
||||||
|
return reviewTransactionResponse.records.map(record => record.get('review'))
|
||||||
})
|
})
|
||||||
const txResult = await reviewWriteTxResultPromise
|
const [reviewed] = await reviewWriteTxResultPromise
|
||||||
if (!txResult[0]) return null
|
return reviewed || null
|
||||||
createdRelationshipWithNestedAttributes = txResult[0]
|
|
||||||
} finally {
|
} finally {
|
||||||
session.close()
|
session.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
return createdRelationshipWithNestedAttributes
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import uuid from 'uuid/v4'
|
import { v4 as uuid } from 'uuid'
|
||||||
import bcrypt from 'bcryptjs'
|
import bcrypt from 'bcryptjs'
|
||||||
import createPasswordReset from './helpers/createPasswordReset'
|
import createPasswordReset from './helpers/createPasswordReset'
|
||||||
|
|
||||||
@ -22,6 +22,7 @@ export default {
|
|||||||
WHERE duration.between(passwordReset.issuedAt, datetime()).days <= 0 AND passwordReset.usedAt IS NULL
|
WHERE duration.between(passwordReset.issuedAt, datetime()).days <= 0 AND passwordReset.usedAt IS NULL
|
||||||
SET passwordReset.usedAt = datetime()
|
SET passwordReset.usedAt = datetime()
|
||||||
SET user.encryptedPassword = $encryptedNewPassword
|
SET user.encryptedPassword = $encryptedNewPassword
|
||||||
|
SET user.updatedAt = toString(datetime())
|
||||||
RETURN passwordReset
|
RETURN passwordReset
|
||||||
`,
|
`,
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import uuid from 'uuid/v4'
|
import { v4 as uuid } from 'uuid'
|
||||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
import { UserInputError } from 'apollo-server'
|
import { UserInputError } from 'apollo-server'
|
||||||
|
|||||||
@ -1,23 +1,13 @@
|
|||||||
import log from './helpers/databaseLogger'
|
import log from './helpers/databaseLogger'
|
||||||
|
|
||||||
const transformReturnType = record => {
|
|
||||||
return {
|
|
||||||
...record.get('report').properties,
|
|
||||||
resource: {
|
|
||||||
__typename: record.get('type'),
|
|
||||||
...record.get('resource').properties,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Mutation: {
|
Mutation: {
|
||||||
fileReport: async (_parent, params, context, _resolveInfo) => {
|
fileReport: async (_parent, params, context, _resolveInfo) => {
|
||||||
const { resourceId, reasonCategory, reasonDescription } = params
|
const { resourceId, reasonCategory, reasonDescription } = params
|
||||||
const { driver, user } = context
|
const { driver, user } = context
|
||||||
const session = driver.session()
|
const session = driver.session()
|
||||||
const reportWriteTxResultPromise = session.writeTransaction(async transaction => {
|
const fileReportWriteTxResultPromise = session.writeTransaction(async transaction => {
|
||||||
const reportTransactionResponse = await transaction.run(
|
const fileReportTransactionResponse = await transaction.run(
|
||||||
`
|
`
|
||||||
MATCH (submitter:User {id: $submitterId})
|
MATCH (submitter:User {id: $submitterId})
|
||||||
MATCH (resource {id: $resourceId})
|
MATCH (resource {id: $resourceId})
|
||||||
@ -27,7 +17,8 @@ export default {
|
|||||||
WITH submitter, resource, report
|
WITH submitter, resource, report
|
||||||
CREATE (report)<-[filed:FILED {createdAt: $createdAt, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription}]-(submitter)
|
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,
|
resourceId,
|
||||||
@ -37,13 +28,12 @@ export default {
|
|||||||
reasonDescription,
|
reasonDescription,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
log(reportTransactionResponse)
|
log(fileReportTransactionResponse)
|
||||||
return reportTransactionResponse.records.map(transformReturnType)
|
return fileReportTransactionResponse.records.map(record => record.get('filedReport'))
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
const [createdRelationshipWithNestedAttributes] = await reportWriteTxResultPromise
|
const [filedReport] = await fileReportWriteTxResultPromise
|
||||||
if (!createdRelationshipWithNestedAttributes) return null
|
return filedReport || null
|
||||||
return createdRelationshipWithNestedAttributes
|
|
||||||
} finally {
|
} finally {
|
||||||
session.close()
|
session.close()
|
||||||
}
|
}
|
||||||
@ -76,14 +66,24 @@ export default {
|
|||||||
filterClause = ''
|
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 =
|
const offset =
|
||||||
params.offset && typeof params.offset === 'number' ? `SKIP ${params.offset}` : ''
|
params.offset && typeof params.offset === 'number' ? `SKIP ${params.offset}` : ''
|
||||||
const limit = params.first && typeof params.first === 'number' ? `LIMIT ${params.first}` : ''
|
const limit = params.first && typeof params.first === 'number' ? `LIMIT ${params.first}` : ''
|
||||||
|
|
||||||
const reportReadTxPromise = session.readTransaction(async transaction => {
|
const reportsReadTxPromise = session.readTransaction(async transaction => {
|
||||||
const allReportsTransactionResponse = await transaction.run(
|
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)
|
MATCH (report:Report)-[:BELONGS_TO]->(resource)
|
||||||
WHERE (resource:User OR resource:Post OR resource:Comment)
|
WHERE (resource:User OR resource:Post OR resource:Comment)
|
||||||
@ -101,11 +101,11 @@ export default {
|
|||||||
${offset} ${limit}
|
${offset} ${limit}
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
log(allReportsTransactionResponse)
|
log(reportsTransactionResponse)
|
||||||
return allReportsTransactionResponse.records.map(record => record.get('report'))
|
return reportsTransactionResponse.records.map(record => record.get('report'))
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
const reports = await reportReadTxPromise
|
const reports = await reportsReadTxPromise
|
||||||
return reports
|
return reports
|
||||||
} finally {
|
} finally {
|
||||||
session.close()
|
session.close()
|
||||||
|
|||||||
@ -10,18 +10,17 @@ const driver = getDriver()
|
|||||||
describe('file a report on a resource', () => {
|
describe('file a report on a resource', () => {
|
||||||
let authenticatedUser, currentUser, mutate, query, moderator, abusiveUser, otherReportingUser
|
let authenticatedUser, currentUser, mutate, query, moderator, abusiveUser, otherReportingUser
|
||||||
const categoryIds = ['cat9']
|
const categoryIds = ['cat9']
|
||||||
const reportMutation = gql`
|
const fileReportMutation = gql`
|
||||||
mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
||||||
fileReport(
|
fileReport(
|
||||||
resourceId: $resourceId
|
resourceId: $resourceId
|
||||||
reasonCategory: $reasonCategory
|
reasonCategory: $reasonCategory
|
||||||
reasonDescription: $reasonDescription
|
reasonDescription: $reasonDescription
|
||||||
) {
|
) {
|
||||||
id
|
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
reasonCategory
|
||||||
closed
|
reasonDescription
|
||||||
rule
|
reportId
|
||||||
resource {
|
resource {
|
||||||
__typename
|
__typename
|
||||||
... on User {
|
... on User {
|
||||||
@ -34,6 +33,35 @@ describe('file a report on a resource', () => {
|
|||||||
content
|
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 {
|
filed {
|
||||||
submitter {
|
submitter {
|
||||||
id
|
id
|
||||||
@ -45,11 +73,31 @@ describe('file a report on a resource', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
const variables = {
|
const reviewMutation = gql`
|
||||||
resourceId: 'whatever',
|
mutation($resourceId: ID!, $disable: Boolean, $closed: Boolean) {
|
||||||
reasonCategory: 'other',
|
review(resourceId: $resourceId, disable: $disable, closed: $closed) {
|
||||||
reasonDescription: 'Violates code of conduct !!!',
|
createdAt
|
||||||
}
|
resource {
|
||||||
|
__typename
|
||||||
|
... on User {
|
||||||
|
id
|
||||||
|
disabled
|
||||||
|
}
|
||||||
|
... on Post {
|
||||||
|
id
|
||||||
|
disabled
|
||||||
|
}
|
||||||
|
... on Comment {
|
||||||
|
id
|
||||||
|
disabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
report {
|
||||||
|
disable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await cleanDatabase()
|
await cleanDatabase()
|
||||||
@ -74,7 +122,7 @@ describe('file a report on a resource', () => {
|
|||||||
describe('unauthenticated', () => {
|
describe('unauthenticated', () => {
|
||||||
it('throws authorization error', async () => {
|
it('throws authorization error', async () => {
|
||||||
authenticatedUser = null
|
authenticatedUser = null
|
||||||
await expect(mutate({ mutation: reportMutation, variables })).resolves.toMatchObject({
|
await expect(mutate({ mutation: fileReportMutation, variables })).resolves.toMatchObject({
|
||||||
data: { fileReport: null },
|
data: { fileReport: null },
|
||||||
errors: [{ message: 'Not Authorised!' }],
|
errors: [{ message: 'Not Authorised!' }],
|
||||||
})
|
})
|
||||||
@ -94,6 +142,17 @@ describe('file a report on a resource', () => {
|
|||||||
password: '1234',
|
password: '1234',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
moderator = await Factory.build(
|
||||||
|
'user',
|
||||||
|
{
|
||||||
|
id: 'moderator-id',
|
||||||
|
role: 'moderator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: 'moderator@example.org',
|
||||||
|
password: '1234',
|
||||||
|
},
|
||||||
|
)
|
||||||
otherReportingUser = await Factory.build(
|
otherReportingUser = await Factory.build(
|
||||||
'user',
|
'user',
|
||||||
{
|
{
|
||||||
@ -127,7 +186,7 @@ describe('file a report on a resource', () => {
|
|||||||
|
|
||||||
describe('invalid resource id', () => {
|
describe('invalid resource id', () => {
|
||||||
it('returns null', async () => {
|
it('returns null', async () => {
|
||||||
await expect(mutate({ mutation: reportMutation, variables })).resolves.toMatchObject({
|
await expect(mutate({ mutation: fileReportMutation, variables })).resolves.toMatchObject({
|
||||||
data: { fileReport: null },
|
data: { fileReport: null },
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
})
|
})
|
||||||
@ -139,47 +198,112 @@ describe('file a report on a resource', () => {
|
|||||||
it('which belongs to resource', async () => {
|
it('which belongs to resource', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: { ...variables, resourceId: 'abusive-user-id' },
|
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
fileReport: {
|
fileReport: {
|
||||||
id: expect.any(String),
|
reportId: expect.any(String),
|
||||||
|
resource: {
|
||||||
|
name: 'abusive-user',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: undefined,
|
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({
|
const firstReport = await mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: { ...variables, resourceId: 'abusive-user-id' },
|
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||||
})
|
})
|
||||||
authenticatedUser = await otherReportingUser.toJson()
|
authenticatedUser = await otherReportingUser.toJson()
|
||||||
const secondReport = await mutate({
|
const secondReport = await mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: { ...variables, resourceId: 'abusive-user-id' },
|
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 () => {
|
describe('report properties are set correctly', () => {
|
||||||
await expect(
|
const reportsCypherQuery =
|
||||||
mutate({
|
'MATCH (resource:User {id: $resourceId})<-[:BELONGS_TO]-(report:Report {closed: false})<-[filed:FILED]-(user:User {id: $currentUserId}) RETURN report'
|
||||||
mutation: reportMutation,
|
|
||||||
|
it('with the rule for how the report will be decided', async () => {
|
||||||
|
await mutate({
|
||||||
|
mutation: fileReportMutation,
|
||||||
variables: { ...variables, resourceId: 'abusive-user-id' },
|
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||||
}),
|
})
|
||||||
).resolves.toMatchObject({
|
|
||||||
data: {
|
const reportsCypherQueryResponse = await instance.cypher(reportsCypherQuery, {
|
||||||
fileReport: {
|
resourceId: 'abusive-user-id',
|
||||||
rule: 'latestReviewUpdatedAtRules',
|
currentUserId: authenticatedUser.id,
|
||||||
},
|
})
|
||||||
},
|
expect(reportsCypherQueryResponse.records).toHaveLength(1)
|
||||||
errors: undefined,
|
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')
|
it.todo('creates multiple filed reports')
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -187,7 +311,7 @@ describe('file a report on a resource', () => {
|
|||||||
it('returns __typename "User"', async () => {
|
it('returns __typename "User"', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: { ...variables, resourceId: 'abusive-user-id' },
|
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
@ -205,7 +329,7 @@ describe('file a report on a resource', () => {
|
|||||||
it('returns user attribute info', async () => {
|
it('returns user attribute info', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: { ...variables, resourceId: 'abusive-user-id' },
|
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).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(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
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,
|
|
||||||
variables: { ...variables, resourceId: 'abusive-user-id' },
|
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
@ -262,7 +364,7 @@ describe('file a report on a resource', () => {
|
|||||||
it('returns the reason category', async () => {
|
it('returns the reason category', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: {
|
variables: {
|
||||||
...variables,
|
...variables,
|
||||||
resourceId: 'abusive-user-id',
|
resourceId: 'abusive-user-id',
|
||||||
@ -272,11 +374,7 @@ describe('file a report on a resource', () => {
|
|||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
fileReport: {
|
fileReport: {
|
||||||
filed: [
|
reasonCategory: 'criminal_behavior_violation_german_law',
|
||||||
{
|
|
||||||
reasonCategory: 'criminal_behavior_violation_german_law',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: undefined,
|
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 () => {
|
it('gives an error if the reason category is not in enum "ReasonCategory"', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: {
|
variables: {
|
||||||
...variables,
|
...variables,
|
||||||
resourceId: 'abusive-user-id',
|
resourceId: 'abusive-user-id',
|
||||||
@ -307,7 +405,7 @@ describe('file a report on a resource', () => {
|
|||||||
it('returns the reason description', async () => {
|
it('returns the reason description', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: {
|
variables: {
|
||||||
...variables,
|
...variables,
|
||||||
resourceId: 'abusive-user-id',
|
resourceId: 'abusive-user-id',
|
||||||
@ -317,11 +415,7 @@ describe('file a report on a resource', () => {
|
|||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
fileReport: {
|
fileReport: {
|
||||||
filed: [
|
reasonDescription: 'My reason!',
|
||||||
{
|
|
||||||
reasonDescription: 'My reason!',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
@ -331,7 +425,7 @@ describe('file a report on a resource', () => {
|
|||||||
it('sanitizes the reason description', async () => {
|
it('sanitizes the reason description', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: {
|
variables: {
|
||||||
...variables,
|
...variables,
|
||||||
resourceId: 'abusive-user-id',
|
resourceId: 'abusive-user-id',
|
||||||
@ -341,11 +435,7 @@ describe('file a report on a resource', () => {
|
|||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
fileReport: {
|
fileReport: {
|
||||||
filed: [
|
reasonDescription: 'My reason !',
|
||||||
{
|
|
||||||
reasonDescription: 'My reason !',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
@ -371,7 +461,7 @@ describe('file a report on a resource', () => {
|
|||||||
it('returns type "Post"', async () => {
|
it('returns type "Post"', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: {
|
variables: {
|
||||||
...variables,
|
...variables,
|
||||||
resourceId: 'post-to-report-id',
|
resourceId: 'post-to-report-id',
|
||||||
@ -392,7 +482,7 @@ describe('file a report on a resource', () => {
|
|||||||
it('returns resource in post attribute', async () => {
|
it('returns resource in post attribute', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: {
|
variables: {
|
||||||
...variables,
|
...variables,
|
||||||
resourceId: 'post-to-report-id',
|
resourceId: 'post-to-report-id',
|
||||||
@ -442,7 +532,7 @@ describe('file a report on a resource', () => {
|
|||||||
it('returns type "Comment"', async () => {
|
it('returns type "Comment"', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: {
|
variables: {
|
||||||
...variables,
|
...variables,
|
||||||
resourceId: 'comment-to-report-id',
|
resourceId: 'comment-to-report-id',
|
||||||
@ -463,7 +553,7 @@ describe('file a report on a resource', () => {
|
|||||||
it('returns resource in comment attribute', async () => {
|
it('returns resource in comment attribute', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: {
|
variables: {
|
||||||
...variables,
|
...variables,
|
||||||
resourceId: 'comment-to-report-id',
|
resourceId: 'comment-to-report-id',
|
||||||
@ -493,7 +583,7 @@ describe('file a report on a resource', () => {
|
|||||||
it('returns null', async () => {
|
it('returns null', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: {
|
variables: {
|
||||||
...variables,
|
...variables,
|
||||||
resourceId: 'tag-to-report-id',
|
resourceId: 'tag-to-report-id',
|
||||||
@ -510,37 +600,6 @@ describe('file a report on a resource', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('query for reported 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 () => {
|
beforeEach(async () => {
|
||||||
authenticatedUser = null
|
authenticatedUser = null
|
||||||
moderator = await Factory.build(
|
moderator = await Factory.build(
|
||||||
@ -632,7 +691,7 @@ describe('file a report on a resource', () => {
|
|||||||
authenticatedUser = await currentUser.toJson()
|
authenticatedUser = await currentUser.toJson()
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: {
|
variables: {
|
||||||
resourceId: 'abusive-post-1',
|
resourceId: 'abusive-post-1',
|
||||||
reasonCategory: 'other',
|
reasonCategory: 'other',
|
||||||
@ -640,7 +699,7 @@ describe('file a report on a resource', () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: {
|
variables: {
|
||||||
resourceId: 'abusive-comment-1',
|
resourceId: 'abusive-comment-1',
|
||||||
reasonCategory: 'discrimination_etc',
|
reasonCategory: 'discrimination_etc',
|
||||||
@ -648,7 +707,7 @@ describe('file a report on a resource', () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: {
|
variables: {
|
||||||
resourceId: 'abusive-user-1',
|
resourceId: 'abusive-user-1',
|
||||||
reasonCategory: 'doxing',
|
reasonCategory: 'doxing',
|
||||||
|
|||||||
@ -251,12 +251,12 @@ export default {
|
|||||||
boolean: {
|
boolean: {
|
||||||
followedByCurrentUser:
|
followedByCurrentUser:
|
||||||
'MATCH (this)<-[:FOLLOWS]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
|
'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:
|
blocked:
|
||||||
'MATCH (this)-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
|
'MATCH (this)-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
|
||||||
isMuted:
|
isMuted:
|
||||||
'MATCH (this)<-[:MUTED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
|
'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: {
|
count: {
|
||||||
contributionsCount:
|
contributionsCount:
|
||||||
|
|||||||
@ -16,3 +16,15 @@ enum ReasonCategory {
|
|||||||
advert_products_services_commercial
|
advert_products_services_commercial
|
||||||
criminal_behavior_violation_german_law
|
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!
|
disable: Boolean!
|
||||||
closed: Boolean!
|
closed: Boolean!
|
||||||
report: Report
|
report: Report
|
||||||
# @cypher(statement: "MATCH (report:Report)<-[this:REVIEWED]-(:User) RETURN report")
|
|
||||||
moderator: User
|
moderator: User
|
||||||
resource: ReviewedResource
|
resource: ReviewedResource
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,9 +5,9 @@ type Report {
|
|||||||
rule: ReportRule!
|
rule: ReportRule!
|
||||||
disable: Boolean!
|
disable: Boolean!
|
||||||
closed: Boolean!
|
closed: Boolean!
|
||||||
filed: [FILED]
|
filed: [FILED]!
|
||||||
reviewed: [REVIEWED]!
|
reviewed: [REVIEWED]!
|
||||||
resource: ReportedResource
|
resource: ReportedResource!
|
||||||
}
|
}
|
||||||
|
|
||||||
union ReportedResource = User | Post | Comment
|
union ReportedResource = User | Post | Comment
|
||||||
@ -16,10 +16,6 @@ enum ReportRule {
|
|||||||
latestReviewUpdatedAtRules
|
latestReviewUpdatedAtRules
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
|
||||||
fileReport(resourceId: ID!, reasonCategory: ReasonCategory!, reasonDescription: String!): Report
|
|
||||||
}
|
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
reports(orderBy: ReportOrdering, first: Int, offset: Int, reviewed: Boolean, closed: Boolean): [Report]
|
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?
|
# Is the currently logged in user following that user?
|
||||||
followedByCurrentUser: Boolean! @cypher(
|
followedByCurrentUser: Boolean! @cypher(
|
||||||
statement: """
|
statement: """
|
||||||
MATCH (this)<-[: FOLLOWS]-(u: User { id: $cypherParams.currentUserId})
|
MATCH (this)<-[:FOLLOWS]-(u:User { id: $cypherParams.currentUserId})
|
||||||
RETURN COUNT(u) >= 1
|
RETURN COUNT(u) >= 1
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
isBlocked: Boolean! @cypher(
|
isBlocked: Boolean! @cypher(
|
||||||
statement: """
|
statement: """
|
||||||
MATCH (this)<-[:BLOCKED]-(user:User {id: $cypherParams.currentUserId})
|
MATCH (this)<-[:BLOCKED]-(user:User {id: $cypherParams.currentUserId})
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"projectId": "qa7fe2",
|
"projectId": "qa7fe2",
|
||||||
"ignoreTestFiles": "*.js",
|
"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"
|
// 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", () => {
|
Then("I should see my comment", () => {
|
||||||
cy.get("div.comment p")
|
cy.get("article.comment-card p")
|
||||||
.should("contain", "Human Connection rocks")
|
.should("contain", "Human Connection rocks")
|
||||||
.get(".user-avatar img")
|
.get(".user-avatar img")
|
||||||
.should("have.attr", "src")
|
.should("have.attr", "src")
|
||||||
@ -40,12 +40,12 @@ Then("I should see my comment", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Then("I should see the entirety of 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")
|
.should("not.contain", "show more")
|
||||||
});
|
});
|
||||||
|
|
||||||
Then("I should see an abreviated version of my comment", () => {
|
Then("I should see an abreviated version of my comment", () => {
|
||||||
cy.get("div.comment")
|
cy.get("article.comment-card")
|
||||||
.should("contain", "show more")
|
.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)=> {
|
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')
|
.find('.content-menu .base-button')
|
||||||
.click()
|
.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) => {
|
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()
|
.parent()
|
||||||
.find("div.ribbon.ribbon--pinned")
|
.parent()
|
||||||
|
.find(".ribbon.--pinned")
|
||||||
.should("contain", "Announcement")
|
.should("contain", "Announcement")
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -111,7 +112,7 @@ Then("I add all required fields", () => {
|
|||||||
.get(".categories-select .base-button")
|
.get(".categories-select .base-button")
|
||||||
.first()
|
.first()
|
||||||
.click()
|
.click()
|
||||||
.get('.ds-flex-item > .ds-form-item .ds-select ')
|
.get('.base-card > .select-field input')
|
||||||
.click()
|
.click()
|
||||||
.get('.ds-select-option')
|
.get('.ds-select-option')
|
||||||
.eq(languages.findIndex(l => l.code === 'en'))
|
.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 => {
|
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')
|
.should("contain", condition === 'updated' ? 'to be updated' : 'new post')
|
||||||
.get(".content")
|
.get(".content")
|
||||||
.should("contain", condition === 'updated' ? 'successfully updated' : 'new post 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')
|
.and("contains", condition === 'updated' ? 'humanconnection' : 'onourjourney')
|
||||||
})
|
})
|
||||||
|
|
||||||
Then("the first image should be removed from the preview", () => {
|
Then("the first image should not be displayed anymore", () => {
|
||||||
cy.fixture("humanconnection.png").as('postTeaserImage').then(function() {
|
cy.get(".hero-image")
|
||||||
cy.get("#postdropzone")
|
.children()
|
||||||
.children()
|
.get('.hero-image > .image')
|
||||||
.get('img.thumbnail-preview')
|
.should('have.length', 1)
|
||||||
.should('have.length', 1)
|
.and('have.attr', 'src')
|
||||||
.and('have.attr', 'src')
|
|
||||||
.and('contain', this.postTeaserImage)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
Then('the {string} post was saved successfully without a teaser image', condition => {
|
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')
|
.should("contain", condition === 'updated' ? 'to be updated' : 'new post')
|
||||||
.get(".content")
|
.get(".content")
|
||||||
.should("contain", condition === 'updated' ? 'successfully updated' : 'new post content')
|
.should("contain", condition === 'updated' ? 'successfully updated' : 'new post content')
|
||||||
.get('.post-page')
|
.get('.post-page')
|
||||||
.should('exist')
|
.should('exist')
|
||||||
.get('.post-page img.ds-card-image')
|
.get('.hero-image > .image')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -156,12 +154,12 @@ Then('I should be able to remove it', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
When('my post has a teaser image', () => {
|
When('my post has a teaser image', () => {
|
||||||
cy.get('.contribution-image')
|
cy.get('.contribution-form .image')
|
||||||
.should('exist')
|
.should('exist')
|
||||||
.and('have.attr', 'src')
|
.and('have.attr', 'src')
|
||||||
})
|
})
|
||||||
|
|
||||||
Then('I should be able to remove the image', () => {
|
Then('I should be able to remove the image', () => {
|
||||||
cy.get('.delete-image')
|
cy.get('.dz-message > .base-button')
|
||||||
.click()
|
.click()
|
||||||
})
|
})
|
||||||
|
|||||||
@ -29,7 +29,7 @@ When("I visit another user's profile page", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Then("I cannot upload a picture", () => {
|
Then("I cannot upload a picture", () => {
|
||||||
cy.get(".ds-card-content")
|
cy.get(".base-card")
|
||||||
.children()
|
.children()
|
||||||
.should("not.have.id", "customdropzone")
|
.should("not.have.id", "customdropzone")
|
||||||
.should("have.class", "user-avatar");
|
.should("have.class", "user-avatar");
|
||||||
|
|||||||
@ -12,7 +12,7 @@ let annoyingUserWhoMutedModeratorTitle = 'Fake news'
|
|||||||
const savePostTitle = $post => {
|
const savePostTitle = $post => {
|
||||||
return $post
|
return $post
|
||||||
.first()
|
.first()
|
||||||
.find('.ds-heading')
|
.find('.title')
|
||||||
.first()
|
.first()
|
||||||
.invoke('text')
|
.invoke('text')
|
||||||
.then(title => {
|
.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', () => {
|
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')
|
.find('.content-menu .base-button')
|
||||||
.click({force: true})
|
.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', () => {
|
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')
|
.get('.user-content-menu .base-button')
|
||||||
.click({ force: true })
|
.click({ force: true })
|
||||||
|
|
||||||
@ -78,7 +78,7 @@ When('I click on the author', () => {
|
|||||||
|
|
||||||
When('I report the author', () => {
|
When('I report the author', () => {
|
||||||
cy.get('.page-name-profile-id-slug').then(() => {
|
cy.get('.page-name-profile-id-slug').then(() => {
|
||||||
invokeReportOnElement('.ds-card').then(() => {
|
invokeReportOnElement('.base-card').then(() => {
|
||||||
cy.get('button')
|
cy.get('button')
|
||||||
.contains('Send')
|
.contains('Send')
|
||||||
.click()
|
.click()
|
||||||
@ -139,7 +139,7 @@ Given('somebody reported the following posts:', table => {
|
|||||||
.authenticateAs(submitter)
|
.authenticateAs(submitter)
|
||||||
.mutate(gql`mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
.mutate(gql`mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
||||||
fileReport(resourceId: $resourceId, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription) {
|
fileReport(resourceId: $resourceId, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription) {
|
||||||
id
|
reportId
|
||||||
}
|
}
|
||||||
}`, {
|
}`, {
|
||||||
resourceId,
|
resourceId,
|
||||||
@ -169,7 +169,7 @@ Then('each list item links to the post page', () => {
|
|||||||
Then('I can visit the post page', () => {
|
Then('I can visit the post page', () => {
|
||||||
cy.contains(annoyingUserWhoMutedModeratorTitle).click()
|
cy.contains(annoyingUserWhoMutedModeratorTitle).click()
|
||||||
cy.location('pathname').should('contain', '/post')
|
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", () => {
|
When("they have a post someone has reported", () => {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { When, Then } from "cypress-cucumber-preprocessor/steps";
|
import { When, Then } from "cypress-cucumber-preprocessor/steps";
|
||||||
When("I search for {string}", value => {
|
When("I search for {string}", value => {
|
||||||
cy.get(".searchable-input .ds-select-search")
|
cy.get(".searchable-input .ds-select input")
|
||||||
.focus()
|
.focus()
|
||||||
.type(value);
|
.type(value);
|
||||||
});
|
});
|
||||||
@ -25,7 +25,7 @@ Then("the search should contain the annoying user", () => {
|
|||||||
expect($li).to.have.length(1);
|
expect($li).to.have.length(1);
|
||||||
})
|
})
|
||||||
cy.get(".ds-select-dropdown .user-teaser .slug").should("contain", '@spammy-spammer');
|
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()
|
.focus()
|
||||||
.type("{esc}");
|
.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 => {
|
When("I type {string} and press Enter", value => {
|
||||||
cy.get(".searchable-input .ds-select-search")
|
cy.get(".searchable-input .ds-select input")
|
||||||
.focus()
|
.focus()
|
||||||
.type(value)
|
.type(value)
|
||||||
.type("{enter}", { force: true });
|
.type("{enter}", { force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
When("I type {string} and press escape", value => {
|
When("I type {string} and press escape", value => {
|
||||||
cy.get(".searchable-input .ds-select-search")
|
cy.get(".searchable-input .ds-select input")
|
||||||
.focus()
|
.focus()
|
||||||
.type(value)
|
.type(value)
|
||||||
.type("{esc}");
|
.type("{esc}");
|
||||||
});
|
});
|
||||||
|
|
||||||
Then("the search field should clear", () => {
|
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", () => {
|
When("I select a post entry", () => {
|
||||||
|
|||||||
@ -80,7 +80,7 @@ Then('I should be on the {string} page', page => {
|
|||||||
.should(loc => {
|
.should(loc => {
|
||||||
expect(loc.pathname).to.eq(page)
|
expect(loc.pathname).to.eq(page)
|
||||||
})
|
})
|
||||||
.get('h3')
|
.get('h2')
|
||||||
.should('contain', 'Social media')
|
.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', () => {
|
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?')
|
.contains('Where else can I find Peter Pan?')
|
||||||
.get('a[href="https://freeradical.zone/peter-pan"]')
|
.get('a[href="https://freeradical.zone/peter-pan"]')
|
||||||
.should('have.length', 1)
|
.should('have.length', 1)
|
||||||
|
|||||||
@ -73,7 +73,7 @@ Given("the {string} user searches for {string}", (_, postTitle) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(user => cy.login(user))
|
.then(user => cy.login(user))
|
||||||
cy.get(".searchable-input .ds-select-search")
|
cy.get(".searchable-input .ds-select input")
|
||||||
.focus()
|
.focus()
|
||||||
.type(postTitle);
|
.type(postTitle);
|
||||||
});
|
});
|
||||||
@ -167,7 +167,8 @@ When("I fill in my email and password combination and click submit", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
When(/(?:when )?I refresh the page/, () => {
|
When(/(?:when )?I refresh the page/, () => {
|
||||||
cy.reload();
|
cy.visit('/')
|
||||||
|
.reload();
|
||||||
});
|
});
|
||||||
|
|
||||||
When("I log out through the menu in the top right corner", () => {
|
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 => {
|
Given("we have the following posts in our database:", table => {
|
||||||
table.hashes().forEach((attributesOrOptions, i) => {
|
table.hashes().forEach((attributesOrOptions, i) => {
|
||||||
cy.factory().build("post", {
|
cy.factory().build("post", {
|
||||||
...attributesOrOptions,
|
...attributesOrOptions,
|
||||||
deleted: Boolean(attributesOrOptions.deleted),
|
deleted: Boolean(attributesOrOptions.deleted),
|
||||||
disabled: Boolean(attributesOrOptions.disabled),
|
disabled: Boolean(attributesOrOptions.disabled),
|
||||||
pinned: Boolean(attributesOrOptions.pinned),
|
pinned: Boolean(attributesOrOptions.pinned),
|
||||||
}, {
|
}, {
|
||||||
...attributesOrOptions,
|
...attributesOrOptions,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
Then("I see a success message:", message => {
|
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) => {
|
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')
|
.click().get('.ds-select-option')
|
||||||
.eq(languages.findIndex(l => l.code === languageCode)).click()
|
.eq(languages.findIndex(l => l.code === languageCode)).click()
|
||||||
})
|
})
|
||||||
|
|
||||||
Then("the post shows up on the landing page at position {int}", index => {
|
Then("the post shows up on the landing page at position {int}", index => {
|
||||||
cy.openPage("landing");
|
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.title);
|
||||||
cy.get(selector).should("contain", lastPost.content);
|
cy.get(selector).should("contain", lastPost.content);
|
||||||
});
|
});
|
||||||
@ -312,16 +313,16 @@ Then("I get redirected to {string}", route => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Then("the post was saved successfully", () => {
|
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);
|
cy.get(".content").should("contain", lastPost.content);
|
||||||
});
|
});
|
||||||
|
|
||||||
Then(/^I should see only ([0-9]+) posts? on the landing page/, postCount => {
|
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 => {
|
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(
|
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", () => {
|
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(".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()
|
.first()
|
||||||
.click({
|
.click({
|
||||||
force: true
|
force: true
|
||||||
@ -424,7 +425,7 @@ When("mention {string} in the text", mention => {
|
|||||||
Then("the notification gets marked as read", () => {
|
Then("the notification gets marked as read", () => {
|
||||||
cy.get(".notifications-menu-popover .notification")
|
cy.get(".notifications-menu-popover .notification")
|
||||||
.first()
|
.first()
|
||||||
.should("have.class", "read");
|
.should("have.class", "--read");
|
||||||
});
|
});
|
||||||
|
|
||||||
Then("there are no notifications in the top menu", () => {
|
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", () => {
|
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");
|
cy.get(".main-container").find(".ds-space.hc-empty");
|
||||||
});
|
});
|
||||||
|
|
||||||
Then("I get removed from his follower collection", () => {
|
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(
|
cy.get(".main-container").contains(
|
||||||
".ds-card-content",
|
".base-card",
|
||||||
"is not followed by anyone"
|
"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", () => {
|
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", () => {
|
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 => {
|
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)
|
.should('have.length', 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
Then('I should see the {string} button', button => {
|
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)
|
.should('contain', button)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -20,7 +20,6 @@ Feature: Notification for a mention
|
|||||||
And I select a category
|
And I select a category
|
||||||
And I choose "en" as the language for the post
|
And I choose "en" as the language for the post
|
||||||
And I click on "Save"
|
And I click on "Save"
|
||||||
When I log out
|
|
||||||
And I log in as "Matt Rider"
|
And I log in as "Matt Rider"
|
||||||
And see 1 unread notifications in the top menu
|
And see 1 unread notifications in the top menu
|
||||||
And open the notification menu and click on the first item
|
And open the notification menu and click on the first item
|
||||||
|
|||||||
@ -35,7 +35,7 @@ Feature: Upload Teaser Image
|
|||||||
And confirm crop
|
And confirm crop
|
||||||
And I should be able to "change" a teaser image
|
And I should be able to "change" a teaser image
|
||||||
And confirm crop
|
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
|
Scenario: Add image, then delete it
|
||||||
When I click on the big plus icon in the bottom right corner to create post
|
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 add all required fields
|
||||||
And I click on "Save"
|
And I click on "Save"
|
||||||
Then I get redirected to ".../new-post"
|
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
|
Scenario: Blocked users should not see link or button to unblock, only blocking users
|
||||||
Given a user has blocked me
|
Given a user has blocked me
|
||||||
When I visit the profile page of the annoying user
|
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 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 }) => {
|
Cypress.Commands.add("manualLogin", ({ email, password }) => {
|
||||||
cy.visit(`/login`);
|
cy.visit(`/login`)
|
||||||
cy.get("input[name=email]")
|
.get("input[name=email]")
|
||||||
.trigger("focus")
|
.trigger("focus")
|
||||||
.type(email);
|
.type(email)
|
||||||
cy.get("input[name=password]")
|
.get("input[name=password]")
|
||||||
.trigger("focus")
|
.trigger("focus")
|
||||||
.type(password);
|
.type(password)
|
||||||
cy.get("button[name=submit]")
|
.get("button[name=submit]")
|
||||||
.as("submitButton")
|
.as("submitButton")
|
||||||
.click();
|
.click();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import Factory, { cleanDatabase } from '../../backend/src/db/factories'
|
import Factory from '../../backend/src/db/factories'
|
||||||
import { getDriver, getNeode } from '../../backend/src/db/neo4j'
|
import { getNeode } from '../../backend/src/db/neo4j'
|
||||||
|
|
||||||
|
|
||||||
const neo4jConfigs = {
|
const neo4jConfigs = {
|
||||||
uri: Cypress.env('NEO4J_URI'),
|
uri: Cypress.env('NEO4J_URI'),
|
||||||
@ -8,7 +9,7 @@ const neo4jConfigs = {
|
|||||||
}
|
}
|
||||||
const neodeInstance = getNeode(neo4jConfigs)
|
const neodeInstance = getNeode(neo4jConfigs)
|
||||||
|
|
||||||
beforeEach(() => cleanDatabase())
|
beforeEach(() => cy.then(() => neodeInstance.cypher('MATCH (everything) DETACH DELETE everything;')))
|
||||||
|
|
||||||
Cypress.Commands.add('neode', () => {
|
Cypress.Commands.add('neode', () => {
|
||||||
return neodeInstance
|
return neodeInstance
|
||||||
|
|||||||
@ -23,4 +23,11 @@ import 'cypress-plugin-retries'
|
|||||||
|
|
||||||
// Alternatively you can use CommonJS syntax:
|
// Alternatively you can use CommonJS syntax:
|
||||||
// require('./commands')
|
// 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",
|
"name": "human-connection",
|
||||||
"version": "0.3.1",
|
"version": "0.4.0",
|
||||||
"description": "Fullstack and API tests with cypress and cucumber for Human Connection",
|
"description": "Fullstack and API tests with cypress and cucumber for Human Connection",
|
||||||
"author": "Human Connection gGmbh",
|
"author": "Human Connection gGmbh",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -18,32 +18,34 @@
|
|||||||
"cypress:backend": "cd backend && yarn run dev",
|
"cypress:backend": "cd backend && yarn run dev",
|
||||||
"cypress:webapp": "cd webapp && yarn run dev",
|
"cypress:webapp": "cd webapp && yarn run dev",
|
||||||
"cypress:setup": "run-p cypress:backend cypress:webapp",
|
"cypress:setup": "run-p cypress:backend cypress:webapp",
|
||||||
"cypress:run": "cross-env cypress run",
|
"cypress:run": "cross-env cypress run --browser firefox",
|
||||||
"cypress:open": "cross-env cypress open",
|
"cypress:open": "cross-env cypress open --browser firefox",
|
||||||
"cucumber:setup": "cd backend && yarn run dev",
|
"cucumber:setup": "cd backend && yarn run dev",
|
||||||
"cucumber": "wait-on tcp:4000 && cucumber-js --require-module @babel/register --exit",
|
"cucumber": "wait-on tcp:4000 && cucumber-js --require-module @babel/register --exit",
|
||||||
"release": "standard-version",
|
"release": "standard-version",
|
||||||
"generate:changelog": "yarn version && auto-changelog"
|
"generate:changelog": "yarn version && auto-changelog"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.8.4",
|
"@babel/core": "^7.8.6",
|
||||||
"@babel/preset-env": "^7.8.4",
|
"@babel/preset-env": "^7.8.4",
|
||||||
"@babel/register": "^7.8.3",
|
"@babel/register": "^7.8.6",
|
||||||
"auto-changelog": "^1.16.2",
|
"auto-changelog": "^1.16.2",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"codecov": "^3.6.5",
|
"codecov": "^3.6.5",
|
||||||
"cross-env": "^6.0.3",
|
"cross-env": "^6.0.3",
|
||||||
"cucumber": "^6.0.5",
|
"cucumber": "^6.0.5",
|
||||||
"cypress": "^3.8.3",
|
"cypress": "^4.0.0",
|
||||||
"cypress-cucumber-preprocessor": "^2.0.1",
|
"cypress-cucumber-preprocessor": "^2.0.1",
|
||||||
"cypress-file-upload": "^3.5.3",
|
"cypress-file-upload": "^3.5.3",
|
||||||
"cypress-plugin-retries": "^1.5.2",
|
"cypress-plugin-retries": "^1.5.2",
|
||||||
"date-fns": "^2.9.0",
|
"date-fns": "^2.10.0",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"expect": "^25.1.0",
|
"expect": "^25.1.0",
|
||||||
"faker": "Marak/faker.js#master",
|
"faker": "Marak/faker.js#master",
|
||||||
"graphql-request": "^1.8.2",
|
"graphql-request": "^1.8.2",
|
||||||
|
"import": "^0.0.6",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
|
"mock-socket": "^9.0.3",
|
||||||
"neo4j-driver": "^4.0.1",
|
"neo4j-driver": "^4.0.1",
|
||||||
"neode": "^0.3.7",
|
"neode": "^0.3.7",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
|
|||||||
@ -9,3 +9,13 @@ button {
|
|||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 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-x-large: 5px;
|
||||||
$border-radius-large: 4px;
|
$border-radius-large: 4px;
|
||||||
$border-radius-base: 4px;
|
$border-radius-base: 4px;
|
||||||
|
$border-radius-small: 2px;
|
||||||
$border-radius-rounded: 2em;
|
$border-radius-rounded: 2em;
|
||||||
$border-radius-circle: 50%;
|
$border-radius-circle: 50%;
|
||||||
|
|
||||||
@ -211,7 +212,8 @@ $letter-spacing-x-small: -0.015em;
|
|||||||
* @presenter Opacity
|
* @presenter Opacity
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$opacity-soft: 0.65;
|
$opacity-base: 1;
|
||||||
|
$opacity-soft: 0.7;
|
||||||
$opacity-disabled: 0.5;
|
$opacity-disabled: 0.5;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -239,6 +241,7 @@ $size-height-large: 50px;
|
|||||||
$size-height-xlarge: 60px;
|
$size-height-xlarge: 60px;
|
||||||
$size-height-footer: 64px;
|
$size-height-footer: 64px;
|
||||||
$size-tappable-square: 44px;
|
$size-tappable-square: 44px;
|
||||||
|
$size-ribbon: 6px;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @tokens Size Width
|
* @tokens Size Width
|
||||||
@ -264,12 +267,23 @@ $size-avatar-large: 114px;
|
|||||||
$size-button-base: 36px;
|
$size-button-base: 36px;
|
||||||
$size-button-small: 26px;
|
$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
|
* @tokens Size Icons
|
||||||
* @presenter Spacing
|
* @presenter Spacing
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$size-icon-base: 16px;
|
$size-icon-base: 16px;
|
||||||
|
$size-icon-large: 60px;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @tokens Shadow
|
* @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-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);
|
$box-shadow-small-inset: inset 0 0 0 1px rgba(0,0,0,.05);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @tokens Effects
|
||||||
|
*/
|
||||||
|
|
||||||
|
$blur-radius: 22px;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @tokens Animation Duration
|
* @tokens Animation Duration
|
||||||
*/
|
*/
|
||||||
@ -316,7 +336,8 @@ $z-index-page-submenu: 2500;
|
|||||||
$z-index-page-header: 2000;
|
$z-index-page-header: 2000;
|
||||||
$z-index-page-sidebar: 1500;
|
$z-index-page-sidebar: 1500;
|
||||||
$z-index-sticky: 100;
|
$z-index-sticky: 100;
|
||||||
$z-index-post-card-link: 5;
|
$z-index-post-teaser-link: 5;
|
||||||
|
$z-index-surface: 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @tokens Media Query
|
* @tokens Media Query
|
||||||
|
|||||||
@ -13,6 +13,8 @@ $easeOut: cubic-bezier(0.19, 1, 0.22, 1);
|
|||||||
content: '';
|
content: '';
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
@ -141,10 +143,9 @@ hr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ds-card .ds-section {
|
.base-card > .ds-section {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin-left: -$space-base;
|
margin: -$space-base;
|
||||||
margin-right: -$space-base;
|
|
||||||
|
|
||||||
.ds-container {
|
.ds-container {
|
||||||
padding: $space-base;
|
padding: $space-base;
|
||||||
|
|||||||
@ -1,30 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="categories-select">
|
<section class="categories-select">
|
||||||
<ds-flex :gutter="{ base: 'xx-small', md: 'small', lg: 'xx-small' }">
|
<base-button
|
||||||
<div v-for="category in categories" :key="category.id">
|
v-for="category in categories"
|
||||||
<ds-flex-item>
|
:key="category.id"
|
||||||
<base-button
|
:data-test="categoryButtonsId(category.id)"
|
||||||
:data-test="categoryButtonsId(category.id)"
|
@click="toggleCategory(category.id)"
|
||||||
@click="toggleCategory(category.id)"
|
:filled="isActive(category.id)"
|
||||||
:filled="isActive(category.id)"
|
:disabled="isDisabled(category.id)"
|
||||||
:disabled="isDisabled(category.id)"
|
:icon="category.icon"
|
||||||
:icon="category.icon"
|
size="small"
|
||||||
size="small"
|
>
|
||||||
>
|
{{ $t(`contribution.category.name.${category.slug}`) }}
|
||||||
{{ $t(`contribution.category.name.${category.slug}`) }}
|
</base-button>
|
||||||
</base-button>
|
</section>
|
||||||
</ds-flex-item>
|
|
||||||
</div>
|
|
||||||
</ds-flex>
|
|
||||||
<p class="small-info">
|
|
||||||
{{
|
|
||||||
$t('contribution.categories.infoSelectedNoOfMaxCategories', {
|
|
||||||
chosen: selectedCount,
|
|
||||||
max: selectedMax,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -85,3 +73,15 @@ export default {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</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 { config, mount } from '@vue/test-utils'
|
||||||
import Comment from './Comment.vue'
|
import CommentCard from './CommentCard.vue'
|
||||||
import Vuex from 'vuex'
|
import Vuex from 'vuex'
|
||||||
|
|
||||||
const localVue = global.localVue
|
const localVue = global.localVue
|
||||||
@ -8,11 +8,17 @@ localVue.directive('scrollTo', jest.fn())
|
|||||||
config.stubs['client-only'] = '<span><slot /></span>'
|
config.stubs['client-only'] = '<span><slot /></span>'
|
||||||
config.stubs['nuxt-link'] = '<span><slot /></span>'
|
config.stubs['nuxt-link'] = '<span><slot /></span>'
|
||||||
|
|
||||||
describe('Comment.vue', () => {
|
describe('CommentCard.vue', () => {
|
||||||
let propsData, mocks, stubs, getters, wrapper, Wrapper
|
let propsData, mocks, stubs, getters, wrapper, Wrapper
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
propsData = {}
|
propsData = {
|
||||||
|
comment: {
|
||||||
|
id: 'comment007',
|
||||||
|
author: { id: 'some-user' },
|
||||||
|
},
|
||||||
|
postId: 'post42',
|
||||||
|
}
|
||||||
mocks = {
|
mocks = {
|
||||||
$t: jest.fn(),
|
$t: jest.fn(),
|
||||||
$toast: {
|
$toast: {
|
||||||
@ -26,6 +32,7 @@ describe('Comment.vue', () => {
|
|||||||
truncate: a => a,
|
truncate: a => a,
|
||||||
removeHtml: a => a,
|
removeHtml: a => a,
|
||||||
},
|
},
|
||||||
|
$route: { hash: '' },
|
||||||
$scrollTo: jest.fn(),
|
$scrollTo: jest.fn(),
|
||||||
$apollo: {
|
$apollo: {
|
||||||
mutate: jest.fn().mockResolvedValue({
|
mutate: jest.fn().mockResolvedValue({
|
||||||
@ -55,7 +62,7 @@ describe('Comment.vue', () => {
|
|||||||
const store = new Vuex.Store({
|
const store = new Vuex.Store({
|
||||||
getters,
|
getters,
|
||||||
})
|
})
|
||||||
return mount(Comment, {
|
return mount(CommentCard, {
|
||||||
store,
|
store,
|
||||||
propsData,
|
propsData,
|
||||||
mocks,
|
mocks,
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { storiesOf } from '@storybook/vue'
|
import { storiesOf } from '@storybook/vue'
|
||||||
import { withA11y } from '@storybook/addon-a11y'
|
import { withA11y } from '@storybook/addon-a11y'
|
||||||
import Comment from './Comment'
|
import CommentCard from './CommentCard'
|
||||||
import helpers from '~/storybook/helpers'
|
import helpers from '~/storybook/helpers'
|
||||||
|
|
||||||
helpers.init()
|
helpers.init()
|
||||||
@ -41,14 +41,14 @@ const comment = {
|
|||||||
__typename: 'Comment',
|
__typename: 'Comment',
|
||||||
}
|
}
|
||||||
|
|
||||||
storiesOf('Comment', module)
|
storiesOf('CommentCard', module)
|
||||||
.addDecorator(withA11y)
|
.addDecorator(withA11y)
|
||||||
.addDecorator(helpers.layout)
|
.addDecorator(helpers.layout)
|
||||||
.add('Basic comment', () => ({
|
.add('Basic comment', () => ({
|
||||||
components: { Comment },
|
components: { CommentCard },
|
||||||
store: helpers.store,
|
store: helpers.store,
|
||||||
data: () => ({
|
data: () => ({
|
||||||
comment,
|
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)
|
expect(closeMethodSpy).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('emits `showEditCommentMenu` event', async () => {
|
it('emits `finishEditing` event', async () => {
|
||||||
wrapper.vm.updateEditorContent('ok')
|
wrapper.vm.updateEditorContent('ok')
|
||||||
await wrapper.find('form').trigger('submit')
|
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)
|
expect(closeMethodSpy).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('emits `showEditCommentMenu` event', async () => {
|
it('emits `finishEditing` event', async () => {
|
||||||
wrapper.vm.updateEditorContent('ok')
|
wrapper.vm.updateEditorContent('ok')
|
||||||
await wrapper.find('[data-test="cancel-button"]').trigger('submit')
|
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>
|
<template>
|
||||||
<ds-form v-model="form" @submit="handleSubmit" class="comment-form">
|
<ds-form v-model="form" @submit="handleSubmit" class="comment-form">
|
||||||
<template slot-scope="{ errors }">
|
<template slot-scope="{ errors }">
|
||||||
<ds-card>
|
<base-card>
|
||||||
<!-- with client-only the content is not shown -->
|
|
||||||
<hc-editor ref="editor" :users="users" :value="form.content" @input="updateEditorContent" />
|
<hc-editor ref="editor" :users="users" :value="form.content" @input="updateEditorContent" />
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<base-button
|
<base-button
|
||||||
@ -17,7 +16,7 @@
|
|||||||
{{ $t('post.comment.submit') }}
|
{{ $t('post.comment.submit') }}
|
||||||
</base-button>
|
</base-button>
|
||||||
</div>
|
</div>
|
||||||
</ds-card>
|
</base-card>
|
||||||
</template>
|
</template>
|
||||||
</ds-form>
|
</ds-form>
|
||||||
</template>
|
</template>
|
||||||
@ -72,7 +71,7 @@ export default {
|
|||||||
this.$refs.editor.clear()
|
this.$refs.editor.clear()
|
||||||
},
|
},
|
||||||
closeEditWindow() {
|
closeEditWindow() {
|
||||||
this.$emit('showEditCommentMenu', false)
|
this.$emit('finishEditing')
|
||||||
},
|
},
|
||||||
handleCancel() {
|
handleCancel() {
|
||||||
if (!this.update) {
|
if (!this.update) {
|
||||||
@ -146,10 +145,13 @@ export default {
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.comment-form {
|
.comment-form {
|
||||||
|
.editor {
|
||||||
|
margin-bottom: $space-small;
|
||||||
|
}
|
||||||
|
|
||||||
.buttons {
|
.buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin: $space-small 0;
|
|
||||||
|
|
||||||
> .base-button {
|
> .base-button {
|
||||||
margin-left: $space-x-small;
|
margin-left: $space-x-small;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { config, mount } from '@vue/test-utils'
|
import { config, mount } from '@vue/test-utils'
|
||||||
import CommentList from './CommentList'
|
import CommentList from './CommentList'
|
||||||
import Comment from '~/components/Comment/Comment'
|
import CommentCard from '~/components/CommentCard/CommentCard'
|
||||||
import Vuex from 'vuex'
|
import Vuex from 'vuex'
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
|
||||||
@ -20,9 +20,14 @@ describe('CommentList.vue', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
propsData = {
|
propsData = {
|
||||||
post: {
|
post: {
|
||||||
id: 1,
|
id: 'post42',
|
||||||
comments: [
|
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,
|
removeHtml: a => a,
|
||||||
},
|
},
|
||||||
$scrollTo: jest.fn(),
|
$scrollTo: jest.fn(),
|
||||||
|
$route: { hash: '' },
|
||||||
$apollo: {
|
$apollo: {
|
||||||
queries: {
|
queries: {
|
||||||
Post: {
|
Post: {
|
||||||
@ -73,12 +79,6 @@ describe('CommentList.vue', () => {
|
|||||||
beforeEach(jest.useFakeTimers)
|
beforeEach(jest.useFakeTimers)
|
||||||
|
|
||||||
describe('$route.hash !== `#comments`', () => {
|
describe('$route.hash !== `#comments`', () => {
|
||||||
beforeEach(() => {
|
|
||||||
mocks.$route = {
|
|
||||||
hash: '',
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('skips $scrollTo', () => {
|
it('skips $scrollTo', () => {
|
||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
jest.runAllTimers()
|
jest.runAllTimers()
|
||||||
@ -107,7 +107,7 @@ describe('CommentList.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Comment emitted reply()', () => {
|
it('Comment emitted reply()', () => {
|
||||||
wrapper.find(Comment).vm.$emit('reply', {
|
wrapper.find(CommentCard).vm.$emit('reply', {
|
||||||
id: 'commentAuthorId',
|
id: 'commentAuthorId',
|
||||||
slug: 'ogerly',
|
slug: 'ogerly',
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,18 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="comments" class="comment-list">
|
<div id="comments" class="comment-list">
|
||||||
<h3 class="title">
|
<h3 class="title">
|
||||||
<counter-icon icon="comments" :count="post.comments.length" />
|
<counter-icon icon="comments" :count="postComments.length" />
|
||||||
{{ $t('common.comment', null, 0) }}
|
{{ $t('common.comment', null, 0) }}
|
||||||
</h3>
|
</h3>
|
||||||
<ds-space margin-bottom="large" />
|
<div v-if="postComments" id="comments" class="comments">
|
||||||
<div v-if="post.comments && post.comments.length" id="comments" class="comments">
|
<comment-card
|
||||||
<comment
|
v-for="comment in postComments"
|
||||||
v-for="comment in post.comments"
|
|
||||||
:key="comment.id"
|
:key="comment.id"
|
||||||
:comment="comment"
|
:comment="comment"
|
||||||
:post="post"
|
:postId="post.id"
|
||||||
:routeHash="routeHash"
|
|
||||||
class="comment-tag"
|
|
||||||
@deleteComment="updateCommentList"
|
@deleteComment="updateCommentList"
|
||||||
@updateComment="updateCommentList"
|
@updateComment="updateCommentList"
|
||||||
@toggleNewCommentForm="toggleNewCommentForm"
|
@toggleNewCommentForm="toggleNewCommentForm"
|
||||||
@ -23,18 +20,25 @@
|
|||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon'
|
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'
|
import scrollToAnchor from '~/mixins/scrollToAnchor'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [scrollToAnchor],
|
mixins: [scrollToAnchor],
|
||||||
components: {
|
components: {
|
||||||
CounterIcon,
|
CounterIcon,
|
||||||
Comment,
|
CommentCard,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
routeHash: { type: String, default: () => '' },
|
post: {
|
||||||
post: { type: Object, default: () => {} },
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
postComments() {
|
||||||
|
return (this.post && this.post.comments) || []
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
reply(message) {
|
reply(message) {
|
||||||
@ -44,7 +48,7 @@ export default {
|
|||||||
return anchor === '#comments'
|
return anchor === '#comments'
|
||||||
},
|
},
|
||||||
updateCommentList(updatedComment) {
|
updateCommentList(updatedComment) {
|
||||||
this.post.comments = this.post.comments.map(comment => {
|
this.postComments = this.postComments.map(comment => {
|
||||||
return comment.id === updatedComment.id ? updatedComment : comment
|
return comment.id === updatedComment.id ? updatedComment : comment
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@ -154,7 +154,7 @@ describe('ContentMenu.vue', () => {
|
|||||||
.filter(item => item.text() === 'comment.menu.edit')
|
.filter(item => item.text() === 'comment.menu.edit')
|
||||||
.at(0)
|
.at(0)
|
||||||
.trigger('click')
|
.trigger('click')
|
||||||
expect(wrapper.emitted('showEditCommentMenu')).toEqual([[true]])
|
expect(wrapper.emitted('editComment')).toBeTruthy()
|
||||||
})
|
})
|
||||||
it('delete the comment', () => {
|
it('delete the comment', () => {
|
||||||
wrapper
|
wrapper
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
size="small"
|
size="small"
|
||||||
circle
|
circle
|
||||||
ghost
|
ghost
|
||||||
@click="toggleMenu"
|
@click.prevent="toggleMenu()"
|
||||||
/>
|
/>
|
||||||
</slot>
|
</slot>
|
||||||
</template>
|
</template>
|
||||||
@ -104,7 +104,7 @@ export default {
|
|||||||
routes.push({
|
routes.push({
|
||||||
label: this.$t(`comment.menu.edit`),
|
label: this.$t(`comment.menu.edit`),
|
||||||
callback: () => {
|
callback: () => {
|
||||||
this.$emit('showEditCommentMenu', true)
|
this.$emit('editComment')
|
||||||
},
|
},
|
||||||
icon: 'edit',
|
icon: 'edit',
|
||||||
})
|
})
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import Vuex from 'vuex'
|
|||||||
import PostMutations from '~/graphql/PostMutations.js'
|
import PostMutations from '~/graphql/PostMutations.js'
|
||||||
import CategoriesSelect from '~/components/CategoriesSelect/CategoriesSelect'
|
import CategoriesSelect from '~/components/CategoriesSelect/CategoriesSelect'
|
||||||
|
|
||||||
import TeaserImage from '~/components/TeaserImage/TeaserImage'
|
import ImageUploader from '~/components/ImageUploader/ImageUploader'
|
||||||
import MutationObserver from 'mutation-observer'
|
import MutationObserver from 'mutation-observer'
|
||||||
|
|
||||||
global.MutationObserver = MutationObserver
|
global.MutationObserver = MutationObserver
|
||||||
@ -182,7 +182,7 @@ describe('ContributionForm.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('has no more than three categories', async () => {
|
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()
|
await Vue.nextTick()
|
||||||
wrapper.find('form').trigger('submit')
|
wrapper.find('form').trigger('submit')
|
||||||
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
|
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
|
||||||
@ -233,10 +233,13 @@ describe('ContributionForm.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('supports adding a teaser image', async () => {
|
it('supports adding a teaser image', async () => {
|
||||||
|
const spy = jest.spyOn(FileReader.prototype, 'readAsDataURL').mockImplementation(() => {})
|
||||||
expectedParams.variables.imageUpload = imageUpload
|
expectedParams.variables.imageUpload = imageUpload
|
||||||
wrapper.find(TeaserImage).vm.$emit('addTeaserImage', imageUpload)
|
wrapper.find(ImageUploader).vm.$emit('addHeroImage', imageUpload)
|
||||||
await wrapper.find('form').trigger('submit')
|
await wrapper.find('form').trigger('submit')
|
||||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
|
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
|
||||||
|
expect(spy).toHaveBeenCalledWith(imageUpload)
|
||||||
|
spy.mockReset()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('content is valid with just a link', async () => {
|
it('content is valid with just a link', async () => {
|
||||||
@ -320,20 +323,12 @@ describe('ContributionForm.vue', () => {
|
|||||||
wrapper = Wrapper()
|
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', () => {
|
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', () => {
|
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', () => {
|
describe('valid update', () => {
|
||||||
@ -362,6 +357,7 @@ describe('ContributionForm.vue', () => {
|
|||||||
image,
|
image,
|
||||||
imageUpload: null,
|
imageUpload: null,
|
||||||
imageAspectRatio: 1,
|
imageAspectRatio: 1,
|
||||||
|
imageBlurred: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -387,9 +383,10 @@ describe('ContributionForm.vue', () => {
|
|||||||
|
|
||||||
it('supports deleting a teaser image', async () => {
|
it('supports deleting a teaser image', async () => {
|
||||||
expectedParams.variables.image = null
|
expectedParams.variables.image = null
|
||||||
|
expectedParams.variables.imageAspectRatio = null
|
||||||
propsData.contribution.image = '/uploads/someimage.png'
|
propsData.contribution.image = '/uploads/someimage.png'
|
||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
wrapper.find('.contribution-form .delete-image').trigger('click')
|
wrapper.find('[data-test="delete-button"]').trigger('click')
|
||||||
await wrapper.find('form').trigger('submit')
|
await wrapper.find('form').trigger('submit')
|
||||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
|
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
|
||||||
})
|
})
|
||||||
|
|||||||
@ -2,115 +2,75 @@
|
|||||||
<ds-form
|
<ds-form
|
||||||
class="contribution-form"
|
class="contribution-form"
|
||||||
ref="contributionForm"
|
ref="contributionForm"
|
||||||
v-model="form"
|
v-model="formData"
|
||||||
:schema="formSchema"
|
:schema="formSchema"
|
||||||
@submit="submit"
|
@submit="submit"
|
||||||
>
|
>
|
||||||
<template slot-scope="{ errors }">
|
<template slot-scope="{ errors }">
|
||||||
<base-button
|
<base-card>
|
||||||
v-if="showDeleteImageButton"
|
<template #heroImage>
|
||||||
class="delete-image"
|
<img
|
||||||
icon="close"
|
v-if="formData.image"
|
||||||
size="small"
|
:src="formData.image | proxyApiUrl"
|
||||||
circle
|
:class="['image', formData.imageBlurred && '--blur-image']"
|
||||||
danger
|
/>
|
||||||
filled
|
<image-uploader
|
||||||
@click.prevent="deleteImage"
|
:hasImage="!!formData.image"
|
||||||
/>
|
:class="[formData.imageBlurred && '--blur-image']"
|
||||||
<hc-teaser-image
|
@addHeroImage="addHeroImage"
|
||||||
:contribution="contribution"
|
@addImageAspectRatio="addImageAspectRatio"
|
||||||
:class="{ '--blur-image': form.blurImage }"
|
/>
|
||||||
@addTeaserImage="addTeaserImage"
|
</template>
|
||||||
@addImageAspectRatio="addImageAspectRatio"
|
<div v-if="formData.image" class="blur-toggle">
|
||||||
@cropInProgress="cropInProgress"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
v-if="contribution"
|
|
||||||
class="contribution-image"
|
|
||||||
:src="contribution.image | proxyApiUrl"
|
|
||||||
/>
|
|
||||||
</hc-teaser-image>
|
|
||||||
|
|
||||||
<ds-card>
|
|
||||||
<div class="blur-toggle">
|
|
||||||
<label for="blur-img">{{ $t('contribution.inappropriatePicture') }}</label>
|
<label for="blur-img">{{ $t('contribution.inappropriatePicture') }}</label>
|
||||||
<input type="checkbox" id="blur-img" v-model="form.blurImage" />
|
<input type="checkbox" id="blur-img" v-model="formData.imageBlurred" />
|
||||||
<p>
|
<a
|
||||||
<a
|
href="https://support.human-connection.org/kb/faq.php?id=113"
|
||||||
href="https://support.human-connection.org/kb/faq.php?id=113"
|
target="_blank"
|
||||||
target="_blank"
|
class="link"
|
||||||
class="link"
|
>
|
||||||
>
|
{{ $t('contribution.inappropriatePictureText') }}
|
||||||
{{ $t('contribution.inappropriatePictureText') }}
|
<base-icon name="question-circle" />
|
||||||
<ds-icon name="question-circle" />
|
</a>
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ds-space />
|
|
||||||
<client-only>
|
|
||||||
<user-teaser :user="currentUser" />
|
|
||||||
</client-only>
|
|
||||||
<ds-space />
|
|
||||||
<ds-input
|
<ds-input
|
||||||
model="title"
|
model="title"
|
||||||
class="post-title"
|
|
||||||
:placeholder="$t('contribution.title')"
|
:placeholder="$t('contribution.title')"
|
||||||
name="title"
|
name="title"
|
||||||
autofocus
|
autofocus
|
||||||
|
size="large"
|
||||||
/>
|
/>
|
||||||
<ds-text align="right">
|
<ds-chip size="base" :color="errors && errors.title && 'danger'">
|
||||||
<ds-chip v-if="errors && errors.title" color="danger" size="base">
|
{{ formData.title.length }}/{{ formSchema.title.max }}
|
||||||
{{ form.title.length }}/{{ formSchema.title.max }}
|
<base-icon v-if="errors && errors.title" name="warning" />
|
||||||
<ds-icon name="warning"></ds-icon>
|
</ds-chip>
|
||||||
</ds-chip>
|
|
||||||
<ds-chip v-else size="base">{{ form.title.length }}/{{ formSchema.title.max }}</ds-chip>
|
|
||||||
</ds-text>
|
|
||||||
<hc-editor
|
<hc-editor
|
||||||
:users="users"
|
:users="users"
|
||||||
:value="form.content"
|
:value="formData.content"
|
||||||
:hashtags="hashtags"
|
:hashtags="hashtags"
|
||||||
@input="updateEditorContent"
|
@input="updateEditorContent"
|
||||||
/>
|
/>
|
||||||
<ds-text align="right">
|
<ds-chip size="base" :color="errors && errors.content && 'danger'">
|
||||||
<ds-chip v-if="errors && errors.content" color="danger" size="base">
|
{{ contentLength }}
|
||||||
{{ contentLength }}
|
<base-icon v-if="errors && errors.content" name="warning" />
|
||||||
<ds-icon name="warning"></ds-icon>
|
</ds-chip>
|
||||||
</ds-chip>
|
<categories-select model="categoryIds" :existingCategoryIds="formData.categoryIds" />
|
||||||
<ds-chip v-else size="base">
|
<ds-chip size="base" :color="errors && errors.categoryIds && 'danger'">
|
||||||
{{ contentLength }}
|
{{ formData.categoryIds.length }} / 3
|
||||||
</ds-chip>
|
<base-icon v-if="errors && errors.categoryIds" name="warning" />
|
||||||
</ds-text>
|
</ds-chip>
|
||||||
<ds-space margin-bottom="small" />
|
<ds-select
|
||||||
<hc-categories-select model="categoryIds" :existingCategoryIds="form.categoryIds" />
|
model="language"
|
||||||
<ds-text align="right">
|
icon="globe"
|
||||||
<ds-chip v-if="errors && errors.categoryIds" color="danger" size="base">
|
class="select-field"
|
||||||
{{ form.categoryIds.length }} / 3
|
:options="languageOptions"
|
||||||
<ds-icon name="warning"></ds-icon>
|
:placeholder="$t('contribution.languageSelectText')"
|
||||||
</ds-chip>
|
:label="$t('contribution.languageSelectLabel')"
|
||||||
<ds-chip v-else size="base">{{ form.categoryIds.length }} / 3</ds-chip>
|
/>
|
||||||
</ds-text>
|
<ds-chip v-if="errors && errors.language" size="base" color="danger">
|
||||||
<ds-flex class="contribution-form-footer">
|
<base-icon name="warning" />
|
||||||
<ds-flex-item :width="{ lg: '50%', md: '50%', sm: '100%' }" />
|
</ds-chip>
|
||||||
<ds-flex-item>
|
<div class="buttons">
|
||||||
<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">
|
|
||||||
<base-button data-test="cancel-button" :disabled="loading" @click="$router.back()" danger>
|
<base-button data-test="cancel-button" :disabled="loading" @click="$router.back()" danger>
|
||||||
{{ $t('actions.cancel') }}
|
{{ $t('actions.cancel') }}
|
||||||
</base-button>
|
</base-button>
|
||||||
@ -118,8 +78,7 @@
|
|||||||
{{ $t('actions.save') }}
|
{{ $t('actions.save') }}
|
||||||
</base-button>
|
</base-button>
|
||||||
</div>
|
</div>
|
||||||
<ds-space margin-bottom="large" />
|
</base-card>
|
||||||
</ds-card>
|
|
||||||
</template>
|
</template>
|
||||||
</ds-form>
|
</ds-form>
|
||||||
</template>
|
</template>
|
||||||
@ -131,127 +90,95 @@ import { mapGetters } from 'vuex'
|
|||||||
import HcEditor from '~/components/Editor/Editor'
|
import HcEditor from '~/components/Editor/Editor'
|
||||||
import locales from '~/locales'
|
import locales from '~/locales'
|
||||||
import PostMutations from '~/graphql/PostMutations.js'
|
import PostMutations from '~/graphql/PostMutations.js'
|
||||||
import HcCategoriesSelect from '~/components/CategoriesSelect/CategoriesSelect'
|
import CategoriesSelect from '~/components/CategoriesSelect/CategoriesSelect'
|
||||||
import HcTeaserImage from '~/components/TeaserImage/TeaserImage'
|
import ImageUploader from '~/components/ImageUploader/ImageUploader'
|
||||||
import UserTeaser from '~/components/UserTeaser/UserTeaser'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
HcEditor,
|
HcEditor,
|
||||||
HcCategoriesSelect,
|
CategoriesSelect,
|
||||||
HcTeaserImage,
|
ImageUploader,
|
||||||
UserTeaser,
|
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
contribution: { type: Object, default: () => {} },
|
contribution: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
image,
|
||||||
|
imageAspectRatio,
|
||||||
|
imageBlurred,
|
||||||
|
language,
|
||||||
|
categories,
|
||||||
|
} = this.contribution
|
||||||
|
|
||||||
const languageOptions = orderBy(locales, 'name').map(locale => {
|
const languageOptions = orderBy(locales, 'name').map(locale => {
|
||||||
return { label: locale.name, value: locale.code }
|
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 {
|
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: {
|
formSchema: {
|
||||||
title: { required: true, min: 3, max: 100 },
|
title: { required: true, min: 3, max: 100 },
|
||||||
content: { required: true },
|
content: { required: true },
|
||||||
categoryIds: {
|
categoryIds: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
required: true,
|
required: true,
|
||||||
validator: (rule, value) => {
|
validator: (_, value = []) => {
|
||||||
const errors = []
|
if (value.length === 0 || value.length > 3) {
|
||||||
if (!(value && value.length >= 1 && value.length <= 3)) {
|
return [new Error(this.$t('common.validations.categories'))]
|
||||||
errors.push(new Error(this.$t('common.validations.categories')))
|
|
||||||
}
|
}
|
||||||
return errors
|
return []
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
language: { required: true },
|
language: { required: true },
|
||||||
blurImage: { required: false },
|
imageBlurred: { required: false },
|
||||||
},
|
},
|
||||||
languageOptions,
|
languageOptions,
|
||||||
id,
|
|
||||||
slug,
|
|
||||||
loading: false,
|
loading: false,
|
||||||
users: [],
|
users: [],
|
||||||
contentMin: 3,
|
|
||||||
hashtags: [],
|
hashtags: [],
|
||||||
elem: null,
|
imageUpload: null,
|
||||||
isCropInProgress: null,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
contentLength() {
|
|
||||||
return this.$filters.removeHtml(this.form.content).length
|
|
||||||
},
|
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
currentUser: 'auth/user',
|
currentUser: 'auth/user',
|
||||||
}),
|
}),
|
||||||
showDeleteImageButton() {
|
contentLength() {
|
||||||
return this.contribution && this.contribution.image && !this.isCropInProgress
|
return this.$filters.removeHtml(this.formData.content).length
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
submit() {
|
submit() {
|
||||||
const {
|
|
||||||
language: { value: language },
|
|
||||||
title,
|
|
||||||
content,
|
|
||||||
image,
|
|
||||||
teaserImage,
|
|
||||||
imageAspectRatio,
|
|
||||||
categoryIds,
|
|
||||||
blurImage,
|
|
||||||
} = this.form
|
|
||||||
this.loading = true
|
this.loading = true
|
||||||
this.$apollo
|
this.$apollo
|
||||||
.mutate({
|
.mutate({
|
||||||
mutation: this.id ? PostMutations().UpdatePost : PostMutations().CreatePost,
|
mutation: this.contribution.id ? PostMutations().UpdatePost : PostMutations().CreatePost,
|
||||||
variables: {
|
variables: {
|
||||||
id: this.id,
|
...this.formData,
|
||||||
title,
|
id: this.contribution.id || null,
|
||||||
content,
|
language: this.formData.language.value,
|
||||||
categoryIds,
|
image: this.imageUpload ? null : this.formData.image,
|
||||||
language,
|
imageUpload: this.imageUpload,
|
||||||
image,
|
|
||||||
imageUpload: teaserImage,
|
|
||||||
imageBlurred: blurImage,
|
|
||||||
imageAspectRatio,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
this.$toast.success(this.$t('contribution.success'))
|
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({
|
this.$router.push({
|
||||||
name: 'post-id-slug',
|
name: 'post-id-slug',
|
||||||
@ -266,22 +193,19 @@ export default {
|
|||||||
updateEditorContent(value) {
|
updateEditorContent(value) {
|
||||||
this.$refs.contributionForm.update('content', value)
|
this.$refs.contributionForm.update('content', value)
|
||||||
},
|
},
|
||||||
addTeaserImage(file) {
|
addHeroImage(file) {
|
||||||
this.form.teaserImage = 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) {
|
addImageAspectRatio(aspectRatio) {
|
||||||
this.form.imageAspectRatio = aspectRatio
|
this.formData.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
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
apollo: {
|
apollo: {
|
||||||
@ -319,41 +243,48 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.contribution-form {
|
.contribution-form > .base-card {
|
||||||
.ds-card-image.--blur-image img {
|
display: flex;
|
||||||
filter: blur(32px);
|
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 {
|
.blur-toggle {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
margin-bottom: $space-base;
|
||||||
|
|
||||||
> .link {
|
> .link {
|
||||||
display: block;
|
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>
|
</style>
|
||||||
|
|||||||
@ -88,7 +88,7 @@ describe('DeleteData.vue', () => {
|
|||||||
|
|
||||||
describe('calls the delete user mutation', () => {
|
describe('calls the delete user mutation', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
enableDeletionInput = wrapper.find('.enable-deletion-input input')
|
enableDeletionInput = wrapper.find('.ds-input')
|
||||||
enableDeletionInput.setValue(deleteAccountName)
|
enableDeletionInput.setValue(deleteAccountName)
|
||||||
deleteAccountBtn = wrapper.find('[data-test="delete-button"]')
|
deleteAccountBtn = wrapper.find('[data-test="delete-button"]')
|
||||||
})
|
})
|
||||||
@ -107,7 +107,7 @@ describe('DeleteData.vue', () => {
|
|||||||
|
|
||||||
it("deletes a user's posts if requested", () => {
|
it("deletes a user's posts if requested", () => {
|
||||||
mocks.$t.mockImplementation(() => deleteContributionsMessage)
|
mocks.$t.mockImplementation(() => deleteContributionsMessage)
|
||||||
enableContributionDeletionCheckbox = wrapper.findAll('.checkbox-container input').at(0)
|
enableContributionDeletionCheckbox = wrapper.findAll('input[type="checkbox"]').at(0)
|
||||||
enableContributionDeletionCheckbox.trigger('click')
|
enableContributionDeletionCheckbox.trigger('click')
|
||||||
deleteAccountBtn.trigger('click')
|
deleteAccountBtn.trigger('click')
|
||||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
|
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
|
||||||
@ -122,7 +122,7 @@ describe('DeleteData.vue', () => {
|
|||||||
|
|
||||||
it("deletes a user's comments if requested", () => {
|
it("deletes a user's comments if requested", () => {
|
||||||
mocks.$t.mockImplementation(() => deleteCommentsMessage)
|
mocks.$t.mockImplementation(() => deleteCommentsMessage)
|
||||||
enableCommentDeletionCheckbox = wrapper.findAll('.checkbox-container input').at(1)
|
enableCommentDeletionCheckbox = wrapper.findAll('input[type="checkbox"]').at(1)
|
||||||
enableCommentDeletionCheckbox.trigger('click')
|
enableCommentDeletionCheckbox.trigger('click')
|
||||||
deleteAccountBtn.trigger('click')
|
deleteAccountBtn.trigger('click')
|
||||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
|
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
|
||||||
@ -137,10 +137,10 @@ describe('DeleteData.vue', () => {
|
|||||||
|
|
||||||
it("deletes a user's posts and comments if requested", () => {
|
it("deletes a user's posts and comments if requested", () => {
|
||||||
mocks.$t.mockImplementation(() => deleteContributionsMessage)
|
mocks.$t.mockImplementation(() => deleteContributionsMessage)
|
||||||
enableContributionDeletionCheckbox = wrapper.findAll('.checkbox-container input').at(0)
|
enableContributionDeletionCheckbox = wrapper.findAll('input[type="checkbox"]').at(0)
|
||||||
enableContributionDeletionCheckbox.trigger('click')
|
enableContributionDeletionCheckbox.trigger('click')
|
||||||
mocks.$t.mockImplementation(() => deleteCommentsMessage)
|
mocks.$t.mockImplementation(() => deleteCommentsMessage)
|
||||||
enableCommentDeletionCheckbox = wrapper.findAll('.checkbox-container input').at(1)
|
enableCommentDeletionCheckbox = wrapper.findAll('input[type="checkbox"]').at(1)
|
||||||
enableCommentDeletionCheckbox.trigger('click')
|
enableCommentDeletionCheckbox.trigger('click')
|
||||||
deleteAccountBtn.trigger('click')
|
deleteAccountBtn.trigger('click')
|
||||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
|
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
|
||||||
@ -166,7 +166,7 @@ describe('DeleteData.vue', () => {
|
|||||||
|
|
||||||
describe('error handling', () => {
|
describe('error handling', () => {
|
||||||
it('shows an error toaster when the mutation rejects', async () => {
|
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)
|
enableDeletionInput.setValue(deleteAccountName)
|
||||||
await Vue.nextTick()
|
await Vue.nextTick()
|
||||||
deleteAccountBtn = wrapper.find('[data-test="delete-button"]')
|
deleteAccountBtn = wrapper.find('[data-test="delete-button"]')
|
||||||
|
|||||||
@ -1,80 +1,46 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<base-card class="delete-data">
|
||||||
<ds-card hover>
|
<h2 class="title">
|
||||||
<ds-space />
|
<base-icon name="warning" />
|
||||||
<ds-container>
|
{{ $t('settings.deleteUserAccount.name') }}
|
||||||
<ds-flex>
|
</h2>
|
||||||
<ds-flex-item :width="{ base: '22%', sm: '12%', md: '12%', lg: '8%' }">
|
<label>
|
||||||
<base-icon name="warning" class="delete-warning-icon" />
|
{{ $t('settings.deleteUserAccount.pleaseConfirm', { confirm: currentUser.name }) }}
|
||||||
</ds-flex-item>
|
</label>
|
||||||
<ds-flex-item :width="{ base: '78%', sm: '88%', md: '88%', lg: '92%' }">
|
<ds-input v-model="enableDeletionValue" />
|
||||||
<ds-heading>{{ $t('settings.deleteUserAccount.name') }}</ds-heading>
|
<p class="notice">{{ $t('settings.deleteUserAccount.accountDescription') }}</p>
|
||||||
</ds-flex-item>
|
<label v-if="currentUser.contributionsCount" class="checkbox">
|
||||||
<ds-space />
|
<input type="checkbox" v-model="deleteContributions" />
|
||||||
<ds-heading tag="h4">
|
{{
|
||||||
{{ $t('settings.deleteUserAccount.accountDescription') }}
|
$t('settings.deleteUserAccount.contributionsCount', {
|
||||||
</ds-heading>
|
count: currentUser.contributionsCount,
|
||||||
</ds-flex>
|
})
|
||||||
</ds-container>
|
}}
|
||||||
<ds-space />
|
</label>
|
||||||
<ds-container>
|
<label v-if="currentUser.commentedCount" class="checkbox">
|
||||||
<transition name="slide-up">
|
<input type="checkbox" v-model="deleteComments" />
|
||||||
<div v-if="deleteEnabled">
|
{{
|
||||||
<label v-if="currentUser.contributionsCount" class="checkbox-container">
|
$t('settings.deleteUserAccount.commentedCount', {
|
||||||
<input type="checkbox" v-model="deleteContributions" />
|
count: currentUser.commentedCount,
|
||||||
<span class="checkmark"></span>
|
})
|
||||||
{{
|
}}
|
||||||
$t('settings.deleteUserAccount.contributionsCount', {
|
</label>
|
||||||
count: currentUser.contributionsCount,
|
<section v-if="deleteEnabled" class="warning">
|
||||||
})
|
<p>{{ $t('settings.deleteUserAccount.accountWarning') }}</p>
|
||||||
}}
|
</section>
|
||||||
</label>
|
<base-button
|
||||||
<ds-space margin-bottom="small" />
|
icon="trash"
|
||||||
<label v-if="currentUser.commentedCount" class="checkbox-container">
|
danger
|
||||||
<input type="checkbox" v-model="deleteComments" />
|
filled
|
||||||
<span class="checkmark"></span>
|
:disabled="!deleteEnabled"
|
||||||
{{
|
data-test="delete-button"
|
||||||
$t('settings.deleteUserAccount.commentedCount', {
|
@click="handleSubmit"
|
||||||
count: currentUser.commentedCount,
|
>
|
||||||
})
|
{{ $t('settings.deleteUserAccount.name') }}
|
||||||
}}
|
</base-button>
|
||||||
</label>
|
</base-card>
|
||||||
<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>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters, mapActions } from 'vuex'
|
import { mapGetters, mapActions } from 'vuex'
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
@ -131,96 +97,47 @@ export default {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.delete-warning-icon {
|
.delete-data {
|
||||||
color: $color-danger;
|
display: flex;
|
||||||
font-size: $font-size-xxx-large;
|
flex-direction: column;
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-container {
|
> .title > .base-icon {
|
||||||
display: block;
|
color: $color-danger;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-container input {
|
> .ds-form-item {
|
||||||
position: absolute;
|
align-self: flex-start;
|
||||||
opacity: 0;
|
margin-top: $space-xxx-small;
|
||||||
cursor: pointer;
|
}
|
||||||
height: 0;
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkmark {
|
> .notice {
|
||||||
position: absolute;
|
font-weight: $font-weight-bold;
|
||||||
top: 0;
|
margin-bottom: $space-small;
|
||||||
left: 0;
|
}
|
||||||
height: 20px;
|
|
||||||
width: 20px;
|
|
||||||
border: 2px solid $background-color-inverse-softer;
|
|
||||||
background-color: $background-color-base;
|
|
||||||
border-radius: $border-radius-x-large;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-container:hover input ~ .checkmark {
|
> .checkbox {
|
||||||
background-color: $background-color-softest;
|
margin-left: $space-base;
|
||||||
}
|
margin-bottom: $space-x-small;
|
||||||
|
|
||||||
.checkbox-container input:checked ~ .checkmark {
|
&:last-of-type {
|
||||||
background-color: $background-color-danger-active;
|
margin-bottom: $space-small;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.checkmark:after {
|
> .warning {
|
||||||
content: '';
|
padding: $space-large;
|
||||||
position: absolute;
|
margin-bottom: $space-small;
|
||||||
display: none;
|
border-radius: $border-radius-base;
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-container input:checked ~ .checkmark:after {
|
color: $color-danger;
|
||||||
display: block;
|
background-color: $color-danger-inverse;
|
||||||
}
|
border-left: 4px solid $color-danger;
|
||||||
|
}
|
||||||
|
|
||||||
.checkbox-container .checkmark:after {
|
> .base-button {
|
||||||
left: 6px;
|
align-self: flex-start;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -40,9 +40,9 @@ storiesOf('Editor', module)
|
|||||||
return {
|
return {
|
||||||
components: { ctx },
|
components: { ctx },
|
||||||
template: `
|
template: `
|
||||||
<ds-card style="width: 50%; min-width: 500px; margin: 0 auto;">
|
<base-card style="width: 50%; min-width: 500px; margin: 0 auto;">
|
||||||
<ctx />
|
<ctx />
|
||||||
</ds-card>
|
</base-card>
|
||||||
`,
|
`,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -89,7 +89,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.hint {
|
&.hint {
|
||||||
opacity: 0.7;
|
opacity: $opacity-soft;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,10 +12,10 @@ describe('FilterMenu.vue', () => {
|
|||||||
mocks = { $t: () => {} }
|
mocks = { $t: () => {} }
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('given a user', () => {
|
describe('given a hashtag', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
propsData = {
|
propsData = {
|
||||||
hashtag: null,
|
hashtag: 'Frieden',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -27,19 +27,14 @@ describe('FilterMenu.vue', () => {
|
|||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not render a card if there are no hashtags', () => {
|
it('renders a card', () => {
|
||||||
expect(wrapper.is('.ds-card')).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('renders a card if there are hashtags', () => {
|
|
||||||
propsData.hashtag = 'Frieden'
|
|
||||||
wrapper = Wrapper()
|
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', () => {
|
it('emits clearSearch', () => {
|
||||||
wrapper.find('[name="clear-search-button"]').trigger('click')
|
wrapper.find('.base-button').trigger('click')
|
||||||
expect(wrapper.emitted().clearSearch).toHaveLength(1)
|
expect(wrapper.emitted().clearSearch).toHaveLength(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,32 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<ds-card class="filter-menu-card">
|
<base-card class="filter-menu">
|
||||||
<ds-flex class="filter-menu-content">
|
<h2>{{ $t('filter-menu.hashtag-search', { hashtag }) }}</h2>
|
||||||
<ds-flex-item>
|
<base-button
|
||||||
<ds-heading size="h3">{{ $t('filter-menu.hashtag-search', { hashtag }) }}</ds-heading>
|
icon="close"
|
||||||
</ds-flex-item>
|
circle
|
||||||
<ds-flex-item>
|
:title="this.$t('filter-menu.clearSearch')"
|
||||||
<div class="filter-menu-buttons">
|
@click="clearSearch"
|
||||||
<base-button
|
/>
|
||||||
name="clear-search-button"
|
</base-card>
|
||||||
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>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
hashtag: { type: String, default: null },
|
hashtag: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
clearSearch() {
|
clearSearch() {
|
||||||
@ -37,21 +27,10 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.filter-menu-card {
|
.filter-menu.base-card {
|
||||||
background-color: $background-color-soft;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-menu-content {
|
|
||||||
height: 100%;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-menu-title {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
padding: $space-x-small $space-base;
|
||||||
|
|
||||||
.filter-menu-buttons {
|
|
||||||
float: right;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import TeaserImage from './TeaserImage.vue'
|
import ImageUploader from './ImageUploader.vue'
|
||||||
|
|
||||||
const localVue = global.localVue
|
const localVue = global.localVue
|
||||||
|
|
||||||
describe('TeaserImage.vue', () => {
|
describe('ImageUploader.vue', () => {
|
||||||
let wrapper
|
let wrapper
|
||||||
let mocks
|
let mocks
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ describe('TeaserImage.vue', () => {
|
|||||||
})
|
})
|
||||||
describe('mount', () => {
|
describe('mount', () => {
|
||||||
const Wrapper = () => {
|
const Wrapper = () => {
|
||||||
return mount(TeaserImage, { mocks, localVue })
|
return mount(ImageUploader, { mocks, localVue })
|
||||||
}
|
}
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
@ -28,21 +28,10 @@ describe('TeaserImage.vue', () => {
|
|||||||
const message = 'File upload failed'
|
const message = 'File upload failed'
|
||||||
const fileError = { status: 'error' }
|
const fileError = { status: 'error' }
|
||||||
|
|
||||||
it('defaults to error false', () => {
|
|
||||||
expect(wrapper.vm.error).toEqual(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('shows an error toaster when verror is called', () => {
|
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)
|
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 LocaleSwitch from './LocaleSwitch.vue'
|
||||||
import Vuex from 'vuex'
|
import Vuex from 'vuex'
|
||||||
|
|
||||||
const localVue = global.localVue
|
const localVue = global.localVue
|
||||||
|
|
||||||
|
config.stubs['client-only'] = '<span><slot /></span>'
|
||||||
|
|
||||||
describe('LocaleSwitch.vue', () => {
|
describe('LocaleSwitch.vue', () => {
|
||||||
let wrapper, mocks, computed, deutschLanguageItem, getters
|
let wrapper, mocks, computed, deutschLanguageItem, getters
|
||||||
|
|
||||||
|
|||||||
@ -1,35 +1,37 @@
|
|||||||
<template>
|
<template>
|
||||||
<dropdown ref="menu" :placement="placement" :offset="offset">
|
<client-only>
|
||||||
<a
|
<dropdown ref="menu" :placement="placement" :offset="offset">
|
||||||
slot="default"
|
<a
|
||||||
slot-scope="{ toggleMenu }"
|
slot="default"
|
||||||
class="locale-menu"
|
slot-scope="{ toggleMenu }"
|
||||||
href="#"
|
class="locale-menu"
|
||||||
@click.prevent="toggleMenu()"
|
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)"
|
|
||||||
>
|
>
|
||||||
{{ item.route.name }}
|
<base-icon name="globe" />
|
||||||
</ds-menu-item>
|
<span class="label">{{ current.code.toUpperCase() }}</span>
|
||||||
</ds-menu>
|
<base-icon class="dropdown-arrow" name="angle-down" />
|
||||||
</dropdown>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@ -1,71 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<ds-container width="medium">
|
<section class="login-form">
|
||||||
<ds-space margin="small">
|
<blockquote>
|
||||||
<blockquote>
|
<p>{{ $t('quotes.african.quote') }}</p>
|
||||||
<p>{{ $t('quotes.african.quote') }}</p>
|
<b>- {{ $t('quotes.african.author') }}</b>
|
||||||
<b>- {{ $t('quotes.african.author') }}</b>
|
</blockquote>
|
||||||
</blockquote>
|
<base-card>
|
||||||
</ds-space>
|
<template #imageColumn>
|
||||||
<ds-card class="login-card">
|
<a :href="$t('login.moreInfoURL')" :title="$t('login.moreInfo')" target="_blank">
|
||||||
<ds-flex gutter="small">
|
<img class="image" alt="Human Connection" src="/img/sign-up/humanconnection.svg" />
|
||||||
<ds-flex-item :width="{ base: '100%', sm: '50%' }" centered>
|
</a>
|
||||||
<client-only>
|
</template>
|
||||||
<locale-switch class="login-locale-switch" offset="5" />
|
<h2 class="title">{{ $t('login.login') }}</h2>
|
||||||
</client-only>
|
<form :disabled="pending" @submit.prevent="onSubmit">
|
||||||
<ds-space margin-top="small" margin-bottom="xxx-small" centered>
|
<ds-input
|
||||||
<img
|
v-model="form.email"
|
||||||
class="login-image"
|
:disabled="pending"
|
||||||
alt="Human Connection"
|
:placeholder="$t('login.email')"
|
||||||
src="/img/sign-up/humanconnection.svg"
|
type="email"
|
||||||
/>
|
name="email"
|
||||||
</ds-space>
|
icon="envelope"
|
||||||
</ds-flex-item>
|
/>
|
||||||
<ds-flex-item :width="{ base: '100%', sm: '50%' }" centered>
|
<ds-input
|
||||||
<ds-space margin="small">
|
v-model="form.password"
|
||||||
<a :href="$t('login.moreInfoURL')" :title="$t('login.moreInfoHint')" target="_blank">
|
:disabled="pending"
|
||||||
{{ $t('login.moreInfo') }}
|
:placeholder="$t('login.password')"
|
||||||
</a>
|
icon="lock"
|
||||||
</ds-space>
|
icon-right="question-circle"
|
||||||
<ds-space margin="small">
|
name="password"
|
||||||
<ds-text size="small">{{ $t('login.copy') }}</ds-text>
|
type="password"
|
||||||
</ds-space>
|
/>
|
||||||
<form :disabled="pending" @submit.prevent="onSubmit">
|
<nuxt-link to="/password-reset/request">
|
||||||
<ds-input
|
{{ $t('login.forgotPassword') }}
|
||||||
v-model="form.email"
|
</nuxt-link>
|
||||||
:disabled="pending"
|
<base-button :loading="pending" filled name="submit" type="submit" icon="sign-in">
|
||||||
:placeholder="$t('login.email')"
|
{{ $t('login.login') }}
|
||||||
type="email"
|
</base-button>
|
||||||
name="email"
|
<p>
|
||||||
icon="envelope"
|
{{ $t('login.no-account') }}
|
||||||
/>
|
<nuxt-link to="/registration/signup">{{ $t('login.register') }}</nuxt-link>
|
||||||
<ds-input
|
</p>
|
||||||
v-model="form.password"
|
</form>
|
||||||
:disabled="pending"
|
<template #topMenu>
|
||||||
:placeholder="$t('login.password')"
|
<locale-switch offset="5" />
|
||||||
icon="lock"
|
</template>
|
||||||
icon-right="question-circle"
|
</base-card>
|
||||||
name="password"
|
</section>
|
||||||
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>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch.vue'
|
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -100,21 +83,16 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.login-image {
|
.login-form {
|
||||||
width: 90%;
|
width: 80vw;
|
||||||
max-width: 200px;
|
max-width: 620px;
|
||||||
}
|
margin: auto;
|
||||||
.login-card {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.base-button {
|
.base-button {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
margin-top: $space-large;
|
||||||
|
margin-bottom: $space-small;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.login-locale-switch {
|
|
||||||
position: absolute;
|
|
||||||
top: 1em;
|
|
||||||
left: 1em;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -63,7 +63,7 @@ describe('Notification', () => {
|
|||||||
|
|
||||||
it('renders reason', () => {
|
it('renders reason', () => {
|
||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
expect(wrapper.find('.reason-text-for-test').text()).toEqual(
|
expect(wrapper.find('.notification > .description').text()).toEqual(
|
||||||
'notifications.reason.commented_on_post',
|
'notifications.reason.commented_on_post',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -79,9 +79,9 @@ describe('Notification', () => {
|
|||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
expect(wrapper.text()).toContain('@dagobert-duck is the best on this comment.')
|
expect(wrapper.text()).toContain('@dagobert-duck is the best on this comment.')
|
||||||
})
|
})
|
||||||
it('has no class "read"', () => {
|
it('has no class "--read"', () => {
|
||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
expect(wrapper.classes()).not.toContain('read')
|
expect(wrapper.classes()).not.toContain('--read')
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('that is read', () => {
|
describe('that is read', () => {
|
||||||
@ -90,8 +90,8 @@ describe('Notification', () => {
|
|||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('has class "read"', () => {
|
it('has class "--read"', () => {
|
||||||
expect(wrapper.classes()).toContain('read')
|
expect(wrapper.classes()).toContain('--read')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -113,7 +113,7 @@ describe('Notification', () => {
|
|||||||
|
|
||||||
it('renders reason', () => {
|
it('renders reason', () => {
|
||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
expect(wrapper.find('.reason-text-for-test').text()).toEqual(
|
expect(wrapper.find('.notification > .description').text()).toEqual(
|
||||||
'notifications.reason.mentioned_in_post',
|
'notifications.reason.mentioned_in_post',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -125,9 +125,9 @@ describe('Notification', () => {
|
|||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
expect(wrapper.text()).toContain('@jenny-rostock is the best on this post.')
|
expect(wrapper.text()).toContain('@jenny-rostock is the best on this post.')
|
||||||
})
|
})
|
||||||
it('has no class "read"', () => {
|
it('has no class "--read"', () => {
|
||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
expect(wrapper.classes()).not.toContain('read')
|
expect(wrapper.classes()).not.toContain('--read')
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('that is read', () => {
|
describe('that is read', () => {
|
||||||
@ -136,8 +136,8 @@ describe('Notification', () => {
|
|||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('has class "read"', () => {
|
it('has class "--read"', () => {
|
||||||
expect(wrapper.classes()).toContain('read')
|
expect(wrapper.classes()).toContain('--read')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -163,7 +163,7 @@ describe('Notification', () => {
|
|||||||
|
|
||||||
it('renders reason', () => {
|
it('renders reason', () => {
|
||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
expect(wrapper.find('.reason-text-for-test').text()).toEqual(
|
expect(wrapper.find('.notification > .description').text()).toEqual(
|
||||||
'notifications.reason.mentioned_in_comment',
|
'notifications.reason.mentioned_in_comment',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -182,9 +182,9 @@ describe('Notification', () => {
|
|||||||
expect(wrapper.text()).toContain('@dagobert-duck is the best on this comment.')
|
expect(wrapper.text()).toContain('@dagobert-duck is the best on this comment.')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('has no class "read"', () => {
|
it('has no class "--read"', () => {
|
||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
expect(wrapper.classes()).not.toContain('read')
|
expect(wrapper.classes()).not.toContain('--read')
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('that is read', () => {
|
describe('that is read', () => {
|
||||||
@ -193,8 +193,8 @@ describe('Notification', () => {
|
|||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('has class "read"', () => {
|
it('has class "--read"', () => {
|
||||||
expect(wrapper.classes()).toContain('read')
|
expect(wrapper.classes()).toContain('--read')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,37 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<ds-space :class="{ read: notification.read, notification: true }" margin-bottom="x-small">
|
<article :class="{ '--read': notification.read, notification: true }">
|
||||||
<client-only>
|
<client-only>
|
||||||
<ds-space margin-bottom="x-small">
|
<user-teaser :user="from.author" :date-time="from.createdAt" />
|
||||||
<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>
|
|
||||||
</client-only>
|
</client-only>
|
||||||
<ds-space margin-bottom="x-small" />
|
<p class="description">{{ $t(`notifications.reason.${notification.reason}`) }}</p>
|
||||||
<nuxt-link
|
<nuxt-link
|
||||||
class="notification-mention-post"
|
class="link"
|
||||||
:to="{ name: 'post-id-slug', params, ...hashParam }"
|
:to="{ name: 'post-id-slug', params, ...hashParam }"
|
||||||
@click.native="$emit('read')"
|
@click.native="$emit('read')"
|
||||||
>
|
>
|
||||||
<ds-space margin-bottom="x-small">
|
<base-card wideContent>
|
||||||
<ds-card
|
<h2 class="title">{{ from.title || from.post.title }}</h2>
|
||||||
:header="from.title || from.post.title"
|
<p>
|
||||||
hover
|
<strong v-if="isComment" class="comment">{{ $t(`notifications.comment`) }}:</strong>
|
||||||
space="x-small"
|
{{ from.contentExcerpt | removeHtml }}
|
||||||
class="notifications-card"
|
</p>
|
||||||
>
|
</base-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>
|
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</ds-space>
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -70,14 +56,36 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.notification.read {
|
.notification {
|
||||||
opacity: $opacity-soft;
|
margin-bottom: $space-base;
|
||||||
}
|
|
||||||
.notifications-card {
|
&:first-of-type {
|
||||||
min-width: 500px;
|
margin-top: $space-x-small;
|
||||||
}
|
}
|
||||||
.comment-notification-header {
|
|
||||||
font-weight: 700;
|
&.--read {
|
||||||
margin-right: 0.1rem;
|
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>
|
</style>
|
||||||
|
|||||||
@ -73,7 +73,7 @@ describe('NotificationList.vue', () => {
|
|||||||
|
|
||||||
describe('click on a notification', () => {
|
describe('click on a notification', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wrapper.find('.notification-mention-post').trigger('click')
|
wrapper.find('.notification > .link').trigger('click')
|
||||||
})
|
})
|
||||||
|
|
||||||
it("emits 'markAsRead' with the id of the notification source", () => {
|
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 { action } from '@storybook/addon-actions'
|
||||||
import NotificationsTable from '~/components/NotificationsTable/NotificationsTable'
|
import NotificationsTable from '~/components/NotificationsTable/NotificationsTable'
|
||||||
import helpers from '~/storybook/helpers'
|
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'
|
import { user } from '~/components/UserTeaser/UserTeaser.story.js'
|
||||||
|
|
||||||
helpers.init()
|
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 Vuex from 'vuex'
|
||||||
|
|
||||||
import PostCard from './PostCard.vue'
|
import PostTeaser from './PostTeaser.vue'
|
||||||
|
|
||||||
const localVue = global.localVue
|
const localVue = global.localVue
|
||||||
|
|
||||||
config.stubs['client-only'] = '<span><slot /></span>'
|
config.stubs['client-only'] = '<span><slot /></span>'
|
||||||
config.stubs['v-popover'] = '<span><slot /></span>'
|
config.stubs['v-popover'] = '<span><slot /></span>'
|
||||||
|
|
||||||
describe('PostCard', () => {
|
describe('PostTeaser', () => {
|
||||||
let store
|
let store
|
||||||
let stubs
|
let stubs
|
||||||
let mocks
|
let mocks
|
||||||
@ -22,11 +22,13 @@ describe('PostCard', () => {
|
|||||||
propsData = {
|
propsData = {
|
||||||
post: {
|
post: {
|
||||||
id: 'p23',
|
id: 'p23',
|
||||||
|
disabled: false,
|
||||||
|
shoutedCount: 0,
|
||||||
|
commentsCount: 0,
|
||||||
name: 'It is a post',
|
name: 'It is a post',
|
||||||
author: {
|
author: {
|
||||||
id: 'u1',
|
id: 'u1',
|
||||||
},
|
},
|
||||||
disabled: false,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
stubs = {
|
stubs = {
|
||||||
@ -55,7 +57,7 @@ describe('PostCard', () => {
|
|||||||
describe('shallowMount', () => {
|
describe('shallowMount', () => {
|
||||||
Wrapper = () => {
|
Wrapper = () => {
|
||||||
store = new Vuex.Store({ getters })
|
store = new Vuex.Store({ getters })
|
||||||
return shallowMount(PostCard, {
|
return shallowMount(PostTeaser, {
|
||||||
store,
|
store,
|
||||||
propsData,
|
propsData,
|
||||||
mocks,
|
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)
|
beforeEach(jest.useFakeTimers)
|
||||||
|
|
||||||
describe('test Post callbacks', () => {
|
describe('test Post callbacks', () => {
|
||||||
@ -99,7 +108,7 @@ describe('PostCard', () => {
|
|||||||
const store = new Vuex.Store({
|
const store = new Vuex.Store({
|
||||||
getters,
|
getters,
|
||||||
})
|
})
|
||||||
return mount(PostCard, {
|
return mount(PostTeaser, {
|
||||||
stubs,
|
stubs,
|
||||||
mocks,
|
mocks,
|
||||||
propsData,
|
propsData,
|
||||||
@ -111,6 +120,7 @@ describe('PostCard', () => {
|
|||||||
describe('given a post', () => {
|
describe('given a post', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
propsData.post = {
|
propsData.post = {
|
||||||
|
...propsData.post,
|
||||||
title: "It's a title",
|
title: "It's a title",
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { storiesOf } from '@storybook/vue'
|
import { storiesOf } from '@storybook/vue'
|
||||||
import { withA11y } from '@storybook/addon-a11y'
|
import { withA11y } from '@storybook/addon-a11y'
|
||||||
import HcPostCard from './PostCard.vue'
|
import PostTeaser from './PostTeaser.vue'
|
||||||
import helpers from '~/storybook/helpers'
|
import helpers from '~/storybook/helpers'
|
||||||
|
|
||||||
helpers.init()
|
helpers.init()
|
||||||
@ -44,24 +44,24 @@ export const post = {
|
|||||||
__typename: 'Post',
|
__typename: 'Post',
|
||||||
}
|
}
|
||||||
|
|
||||||
storiesOf('Post Card', module)
|
storiesOf('PostTeaser', module)
|
||||||
.addDecorator(withA11y)
|
.addDecorator(withA11y)
|
||||||
.addDecorator(helpers.layout)
|
.addDecorator(helpers.layout)
|
||||||
.add('without image', () => ({
|
.add('without image', () => ({
|
||||||
components: { HcPostCard },
|
components: { PostTeaser },
|
||||||
store: helpers.store,
|
store: helpers.store,
|
||||||
data: () => ({
|
data: () => ({
|
||||||
post,
|
post,
|
||||||
}),
|
}),
|
||||||
template: `
|
template: `
|
||||||
<hc-post-card
|
<post-teaser
|
||||||
:post="post"
|
:post="post"
|
||||||
:width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }"
|
:width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }"
|
||||||
/>
|
/>
|
||||||
`,
|
`,
|
||||||
}))
|
}))
|
||||||
.add('with image', () => ({
|
.add('with image', () => ({
|
||||||
components: { HcPostCard },
|
components: { PostTeaser },
|
||||||
store: helpers.store,
|
store: helpers.store,
|
||||||
data: () => ({
|
data: () => ({
|
||||||
post: {
|
post: {
|
||||||
@ -70,27 +70,23 @@ storiesOf('Post Card', module)
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
template: `
|
template: `
|
||||||
<hc-post-card
|
<post-teaser
|
||||||
:post="post"
|
:post="post"
|
||||||
:width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }"
|
:width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }"
|
||||||
/>
|
/>
|
||||||
`,
|
`,
|
||||||
}))
|
}))
|
||||||
.add('pinned by admin', () => ({
|
.add('pinned by admin', () => ({
|
||||||
components: { HcPostCard },
|
components: { PostTeaser },
|
||||||
store: helpers.store,
|
store: helpers.store,
|
||||||
data: () => ({
|
data: () => ({
|
||||||
post: {
|
post: {
|
||||||
...post,
|
...post,
|
||||||
pinnedBy: {
|
pinned: true,
|
||||||
id: '4711',
|
|
||||||
name: 'Ad Min',
|
|
||||||
role: 'admin',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
template: `
|
template: `
|
||||||
<hc-post-card
|
<post-teaser
|
||||||
:post="post"
|
:post="post"
|
||||||
:width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }"
|
: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>
|
<template>
|
||||||
<div class="ribbon">
|
<aside class="ribbon">
|
||||||
<small>{{ text }}</small>
|
<p>{{ text }}</p>
|
||||||
</div>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -18,40 +18,29 @@ export default {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.ribbon {
|
.ribbon {
|
||||||
$card-shadow: 0px 12px 26px -4px rgba(0, 0, 0, 0.1);
|
padding: $size-ribbon $size-ribbon;
|
||||||
//position: absolute;
|
border-radius: $border-radius-small 0 0 $border-radius-small;
|
||||||
position: relative;
|
color: $color-neutral-100;
|
||||||
right: -31px;
|
|
||||||
top: -31px;
|
|
||||||
|
|
||||||
font-size: 0.9em;
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 6px 6px;
|
|
||||||
color: #fff;
|
|
||||||
background-color: $background-color-secondary-active;
|
background-color: $background-color-secondary-active;
|
||||||
float: right;
|
font-size: $font-size-x-small;
|
||||||
border-radius: 2px 0 0 2px;
|
font-weight: $font-weight-bold;
|
||||||
box-shadow: $card-shadow;
|
|
||||||
z-index: 11;
|
&::before {
|
||||||
// border: 1px solid #ccc;
|
|
||||||
&:before {
|
|
||||||
content: ' ';
|
content: ' ';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: -6px;
|
bottom: -$size-ribbon;
|
||||||
border-width: 3px 4px 3px 3px;
|
border-width: $border-size-large 4px $border-size-large $border-size-large;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-color: $background-color-secondary transparent transparent $background-color-secondary;
|
border-color: $background-color-secondary transparent transparent $background-color-secondary;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon--pinned {
|
&.--pinned {
|
||||||
background-color: $color-warning-active;
|
background-color: $color-warning;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
border-color: $color-warning transparent transparent $color-warning;
|
border-color: $color-warning transparent transparent $color-warning;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</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: `
|
template: `
|
||||||
<user-teaser :user="user" :date-time="new Date()">
|
<user-teaser :user="user" :date-time="new Date()">
|
||||||
<template v-slot:dateTime>
|
<template #dateTime>
|
||||||
- HEY! I'm edited
|
- HEY! I'm edited
|
||||||
</template>
|
</template>
|
||||||
</user>
|
</user>
|
||||||
|
|||||||
@ -3,107 +3,36 @@
|
|||||||
<user-avatar v-if="showAvatar" size="small" />
|
<user-avatar v-if="showAvatar" size="small" />
|
||||||
<span class="info anonymous">{{ $t('profile.userAnonym') }}</span>
|
<span class="info anonymous">{{ $t('profile.userAnonym') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<dropdown
|
<div v-else :class="[{ 'disabled-content': user.disabled }]" placement="top-start">
|
||||||
v-else
|
<nuxt-link :to="userLink" :class="['user-teaser']">
|
||||||
:class="[{ 'disabled-content': user.disabled }]"
|
<user-avatar v-if="showAvatar" :user="user" size="small" />
|
||||||
placement="top-start"
|
<div class="info">
|
||||||
offset="0"
|
<span class="text">
|
||||||
>
|
<span class="slug">{{ userSlug }}</span>
|
||||||
<template #default="{ openMenu, closeMenu, isOpen }">
|
<span v-if="dateTime">{{ userName }}</span>
|
||||||
<nuxt-link
|
</span>
|
||||||
:to="userLink"
|
<span v-if="dateTime" class="text">
|
||||||
:class="['user-teaser', isOpen && 'active']"
|
<base-icon name="clock" />
|
||||||
@mouseover.native="showPopover ? openMenu(true) : () => {}"
|
<hc-relative-date-time :date-time="dateTime" />
|
||||||
@mouseleave.native="closeMenu(true)"
|
<slot name="dateTime"></slot>
|
||||||
>
|
</span>
|
||||||
<user-avatar v-if="showAvatar" :user="user" size="small" />
|
<span v-else class="text">{{ userName }}</span>
|
||||||
<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>
|
</div>
|
||||||
</template>
|
</nuxt-link>
|
||||||
</dropdown>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
import HcRelativeDateTime from '~/components/RelativeDateTime'
|
import HcRelativeDateTime from '~/components/RelativeDateTime'
|
||||||
import HcFollowButton from '~/components/FollowButton'
|
|
||||||
import HcBadges from '~/components/Badges'
|
|
||||||
import UserAvatar from '~/components/_new/generic/UserAvatar/UserAvatar'
|
import UserAvatar from '~/components/_new/generic/UserAvatar/UserAvatar'
|
||||||
import Dropdown from '~/components/Dropdown'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'UserTeaser',
|
name: 'UserTeaser',
|
||||||
components: {
|
components: {
|
||||||
HcRelativeDateTime,
|
HcRelativeDateTime,
|
||||||
HcFollowButton,
|
|
||||||
UserAvatar,
|
UserAvatar,
|
||||||
HcBadges,
|
|
||||||
Dropdown,
|
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
user: { type: Object, default: null },
|
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 { storiesOf } from '@storybook/vue'
|
||||||
import { withA11y } from '@storybook/addon-a11y'
|
import { withA11y } from '@storybook/addon-a11y'
|
||||||
import { action } from '@storybook/addon-actions'
|
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 { user } from '~/components/UserTeaser/UserTeaser.story.js'
|
||||||
import helpers from '~/storybook/helpers'
|
import helpers from '~/storybook/helpers'
|
||||||
import ReportList from './ReportList'
|
import ReportList from './ReportList'
|
||||||
@ -183,11 +183,11 @@ storiesOf('ReportList', module)
|
|||||||
openModal: action('openModal'),
|
openModal: action('openModal'),
|
||||||
filter: action('filter'),
|
filter: action('filter'),
|
||||||
},
|
},
|
||||||
template: `<ds-card>
|
template: `<base-card>
|
||||||
<div class="reports-header">
|
<div class="reports-header">
|
||||||
<h3 class="title">Reports</h3>
|
<h3 class="title">Reports</h3>
|
||||||
<dropdown-filter @filter="filter" :filterOptions="filterOptions" :selected="selected" />
|
<dropdown-filter @filter="filter" :filterOptions="filterOptions" :selected="selected" />
|
||||||
</div>
|
</div>
|
||||||
<reports-table :reports="reports" @confirm="openModal" />
|
<reports-table :reports="reports" @confirm="openModal" />
|
||||||
</ds-card>`,
|
</base-card>`,
|
||||||
}))
|
}))
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<ds-card>
|
<base-card>
|
||||||
<div class="reports-header">
|
<div class="reports-header">
|
||||||
<h3 class="title">{{ $t('moderation.reports.name') }}</h3>
|
<h3 class="title">{{ $t('moderation.reports.name') }}</h3>
|
||||||
<client-only>
|
<client-only>
|
||||||
@ -8,8 +8,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<reports-table :reports="reports" @confirm="openModal" />
|
<reports-table :reports="reports" @confirm="openModal" />
|
||||||
<pagination-buttons :hasNext="hasNext" :hasPrevious="hasPrevious" @back="back" @next="next" />
|
<pagination-buttons :hasNext="hasNext" :hasPrevious="hasPrevious" @back="back" @next="next" />
|
||||||
</ds-card>
|
</base-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapMutations } from 'vuex'
|
import { mapMutations } from 'vuex'
|
||||||
import DropdownFilter from '~/components/DropdownFilter/DropdownFilter'
|
import DropdownFilter from '~/components/DropdownFilter/DropdownFilter'
|
||||||
@ -43,13 +44,22 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
filterOptions() {
|
filterOptions() {
|
||||||
return [
|
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'),
|
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() {
|
modalData() {
|
||||||
@ -107,13 +117,8 @@ export default {
|
|||||||
filter(option) {
|
filter(option) {
|
||||||
this.selected = option.label
|
this.selected = option.label
|
||||||
this.offset = 0
|
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.reviewed = option.value.reviewed
|
||||||
|
this.closed = option.value.closed
|
||||||
},
|
},
|
||||||
async confirmCallback(resource) {
|
async confirmCallback(resource) {
|
||||||
const { disabled: disable, id: resourceId } = resource
|
const { disabled: disable, id: resourceId } = resource
|
||||||
@ -166,7 +171,7 @@ export default {
|
|||||||
.reports-header {
|
.reports-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin: $space-small 0;
|
margin-bottom: $space-small;
|
||||||
|
|
||||||
> .title {
|
> .title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@ -24,11 +24,8 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<template v-for="report in reports">
|
<template v-for="report in reports">
|
||||||
<report-row
|
<!-- 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 -->
|
||||||
:key="report.resource.id"
|
<report-row :key="report.id" :report="report" @confirm-report="$emit('confirm', report)" />
|
||||||
:report="report"
|
|
||||||
@confirm-report="$emit('confirm', report)"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</table>
|
</table>
|
||||||
<hc-empty v-else icon="alert" :message="$t('moderation.reports.empty')" />
|
<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"
|
// 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
|
reasonCategory: $reasonCategory
|
||||||
reasonDescription: $reasonDescription
|
reasonDescription: $reasonDescription
|
||||||
) {
|
) {
|
||||||
id
|
reportId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|||||||
@ -10,9 +10,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</ds-flex-item>
|
</ds-flex-item>
|
||||||
<ds-flex-item width="20%" style="flex-grow:0;">
|
<ds-flex-item width="20%" style="flex-grow:0;">
|
||||||
<client-only>
|
<locale-switch class="topbar-locale-switch" placement="top" offset="16" />
|
||||||
<locale-switch class="topbar-locale-switch" placement="top" offset="16" />
|
|
||||||
</client-only>
|
|
||||||
</ds-flex-item>
|
</ds-flex-item>
|
||||||
</ds-flex>
|
</ds-flex>
|
||||||
</ds-container>
|
</ds-container>
|
||||||
|
|||||||
@ -50,9 +50,7 @@
|
|||||||
}"
|
}"
|
||||||
style="flex-basis: auto;"
|
style="flex-basis: auto;"
|
||||||
>
|
>
|
||||||
<client-only>
|
<locale-switch class="topbar-locale-switch" placement="top" offset="8" />
|
||||||
<locale-switch class="topbar-locale-switch" placement="top" offset="8" />
|
|
||||||
</client-only>
|
|
||||||
<template v-if="isLoggedIn">
|
<template v-if="isLoggedIn">
|
||||||
<client-only>
|
<client-only>
|
||||||
<notification-menu placement="top" />
|
<notification-menu placement="top" />
|
||||||
|
|||||||
@ -40,7 +40,7 @@
|
|||||||
"tagCountUnique": "Benutzer"
|
"tagCountUnique": "Benutzer"
|
||||||
},
|
},
|
||||||
"invites": {
|
"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",
|
"name": "Benutzer einladen",
|
||||||
"title": "Leute einladen"
|
"title": "Leute einladen"
|
||||||
},
|
},
|
||||||
@ -66,7 +66,7 @@
|
|||||||
"table": {
|
"table": {
|
||||||
"columns": {
|
"columns": {
|
||||||
"createdAt": "Erstellt am",
|
"createdAt": "Erstellt am",
|
||||||
"email": "E-mail",
|
"email": "E-Mail",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"number": "Nr.",
|
"number": "Nr.",
|
||||||
"role": "Rolle",
|
"role": "Rolle",
|
||||||
@ -81,9 +81,9 @@
|
|||||||
"list": {
|
"list": {
|
||||||
"0": "Aufforderung zum sofortigen Abstellen des inakzeptablen Verhaltens",
|
"0": "Aufforderung zum sofortigen Abstellen des inakzeptablen Verhaltens",
|
||||||
"1": "Sperren oder Löschen von Kommentaren",
|
"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",
|
"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",
|
"5": "Vorübergehender Ausschluss aus dem Netzwerk",
|
||||||
"6": "Endgültiger Ausschluss aus dem Netzwerk",
|
"6": "Endgültiger Ausschluss aus dem Netzwerk",
|
||||||
"7": "Verstöße gegen deutsches Recht können zur Anzeige gebracht werden."
|
"7": "Verstöße gegen deutsches Recht können zur Anzeige gebracht werden."
|
||||||
@ -93,10 +93,10 @@
|
|||||||
"expected-behaviour": {
|
"expected-behaviour": {
|
||||||
"description": "Die folgenden Verhaltensweisen werden von allen Community-Mitgliedern erwartet und gefordert:",
|
"description": "Die folgenden Verhaltensweisen werden von allen Community-Mitgliedern erwartet und gefordert:",
|
||||||
"list": {
|
"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.",
|
"1": "Versuche auf andere zuzugehen, bevor ein Konflikt entsteht.",
|
||||||
"2": "Vermeide erniedrigende, diskriminierende oder belästigende Verhaltensweisen und Ausdrücke.",
|
"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"
|
"title": "Erwartetes Verhalten"
|
||||||
},
|
},
|
||||||
@ -114,9 +114,9 @@
|
|||||||
"description": "Die folgenden Verhaltensweisen sind in unserer Community inakzeptabel:",
|
"description": "Die folgenden Verhaltensweisen sind in unserer Community inakzeptabel:",
|
||||||
"list": {
|
"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.",
|
"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.",
|
"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.",
|
"4": "Absichtliche Einschüchterung, Stalking oder Verfolgung.",
|
||||||
"5": "Bewerben von Produkten und Dienstleistungen mit kommerzieller Absicht.",
|
"5": "Bewerben von Produkten und Dienstleistungen mit kommerzieller Absicht.",
|
||||||
"6": "Strafbares Verhalten bzw. Verstoß gegen deutsches Recht.",
|
"6": "Strafbares Verhalten bzw. Verstoß gegen deutsches Recht.",
|
||||||
@ -157,7 +157,7 @@
|
|||||||
"user": "Benutzer ::: Benutzer",
|
"user": "Benutzer ::: Benutzer",
|
||||||
"validations": {
|
"validations": {
|
||||||
"categories": "es müssen eine bis drei Kategorien ausgewählt werden",
|
"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"
|
"url": "muss eine gültige URL sein"
|
||||||
},
|
},
|
||||||
"versus": "Versus"
|
"versus": "Versus"
|
||||||
@ -165,7 +165,7 @@
|
|||||||
"components": {
|
"components": {
|
||||||
"enter-nonce": {
|
"enter-nonce": {
|
||||||
"form": {
|
"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",
|
"next": "Weiter",
|
||||||
"nonce": "Code eingeben",
|
"nonce": "Code eingeben",
|
||||||
"validations": {
|
"validations": {
|
||||||
@ -181,9 +181,9 @@
|
|||||||
},
|
},
|
||||||
"request": {
|
"request": {
|
||||||
"form": {
|
"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",
|
"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"
|
"title": "Passwort zurücksetzen"
|
||||||
}
|
}
|
||||||
@ -191,25 +191,25 @@
|
|||||||
"registration": {
|
"registration": {
|
||||||
"create-user-account": {
|
"create-user-account": {
|
||||||
"error": "Es konnte kein Benutzerkonto erstellt werden!",
|
"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!",
|
"success": "Dein Benutzerkonto wurde erstellt!",
|
||||||
"title": "Benutzerkonto anlegen"
|
"title": "Benutzerkonto anlegen"
|
||||||
},
|
},
|
||||||
"signup": {
|
"signup": {
|
||||||
"form": {
|
"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",
|
"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:",
|
"description": "Um loszulegen, kannst Du Dich hier kostenfrei registrieren:",
|
||||||
"errors": {
|
"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."
|
"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>",
|
"invitation-code": "Dein Einladungscode lautet: <b>{code}</b>",
|
||||||
"minimum-age": "Ich bin 18 Jahre oder älter.",
|
"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-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",
|
"submit": "Konto erstellen",
|
||||||
"success": "Eine Mail mit einem Bestätigungslink für die Registrierung wurde an <b>{email}</b> geschickt",
|
"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."
|
"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!",
|
"title": "Mach mit bei Human Connection!",
|
||||||
"unavailable": "Leider ist die öffentliche Registrierung von Benutzerkonten auf diesem Server derzeit nicht möglich."
|
"unavailable": "Leider ist die öffentliche Registrierung von Benutzerkonten auf diesem Server derzeit nicht möglich."
|
||||||
@ -217,6 +217,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"contribution": {
|
"contribution": {
|
||||||
|
"amount-comments": "{amount} comments",
|
||||||
|
"amount-shouts": "{amount} recommendations",
|
||||||
"categories": {
|
"categories": {
|
||||||
"infoSelectedNoOfMaxCategories": "{chosen} von {max} Kategorien ausgewählt"
|
"infoSelectedNoOfMaxCategories": "{chosen} von {max} Kategorien ausgewählt"
|
||||||
},
|
},
|
||||||
@ -248,10 +250,10 @@
|
|||||||
"surprised": "Erstaunt"
|
"surprised": "Erstaunt"
|
||||||
},
|
},
|
||||||
"filterALL": "Alle Beiträge anzeigen",
|
"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.",
|
"inappropriatePicture": "Dieses Bild kann für einige Menschen unangemessen sein.",
|
||||||
"inappropriatePictureText": "Wann soll ein Foto versteckt werden",
|
"inappropriatePictureText": "Wann sollte mein Beitragsbild verschwommen sein?",
|
||||||
"languageSelectLabel": "Sprache deines Beitrags",
|
"languageSelectLabel": "Sprache Deines Beitrags",
|
||||||
"languageSelectText": "Sprache wählen",
|
"languageSelectText": "Sprache wählen",
|
||||||
"newPost": "Erstelle einen neuen Beitrag",
|
"newPost": "Erstelle einen neuen Beitrag",
|
||||||
"success": "Gespeichert!",
|
"success": "Gespeichert!",
|
||||||
@ -263,16 +265,16 @@
|
|||||||
"delete": {
|
"delete": {
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"comment": {
|
"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!",
|
"success": "Kommentar erfolgreich gelöscht!",
|
||||||
"title": "Lösche Kommentar",
|
"title": "Lösche Kommentar",
|
||||||
"type": "Comment"
|
"type": "Kommentar"
|
||||||
},
|
},
|
||||||
"contribution": {
|
"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!",
|
"success": "Beitrag erfolgreich gelöscht!",
|
||||||
"title": "Lösche Beitrag",
|
"title": "Lösche Beitrag",
|
||||||
"type": "Contribution"
|
"type": "Beitrag"
|
||||||
},
|
},
|
||||||
"submit": "Löschen"
|
"submit": "Löschen"
|
||||||
},
|
},
|
||||||
@ -297,15 +299,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"donations": {
|
"donations": {
|
||||||
"amount-of-total": "{amount} von {total} € erreicht",
|
"amount-of-total": "{amount} von {total} € erreicht",
|
||||||
"donate-now": "Jetzt spenden",
|
"donate-now": "Jetzt spenden",
|
||||||
"donations-for": "Spenden für"
|
"donations-for": "Spenden für"
|
||||||
},
|
},
|
||||||
"editor": {
|
"editor": {
|
||||||
"embed": {
|
"embed": {
|
||||||
"always_allow": "Inhalte von Drittanbietern immer anzeigen (diese Einstellung kannst du jederzeit ändern)",
|
"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_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!",
|
"data_privacy_warning": "Achte auf Deine Daten!",
|
||||||
"play_now": "Jetzt ansehen"
|
"play_now": "Jetzt ansehen"
|
||||||
},
|
},
|
||||||
"hashtag": {
|
"hashtag": {
|
||||||
@ -355,7 +357,7 @@
|
|||||||
"following": "Folge Ich"
|
"following": "Folge Ich"
|
||||||
},
|
},
|
||||||
"index": {
|
"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."
|
"no-results": "Keine Beiträge gefunden."
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
@ -375,8 +377,8 @@
|
|||||||
"success": "Du bist eingeloggt!"
|
"success": "Du bist eingeloggt!"
|
||||||
},
|
},
|
||||||
"maintenance": {
|
"maintenance": {
|
||||||
"explanation": "Zurzeit führen wir einige geplante Wartungsarbeiten durch, bitte versuch es später erneut.",
|
"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",
|
"questions": "Bei Fragen oder Problemen erreichst Du uns per E-Mail an",
|
||||||
"title": "Human Connection befindet sich in der Wartung"
|
"title": "Human Connection befindet sich in der Wartung"
|
||||||
},
|
},
|
||||||
"moderation": {
|
"moderation": {
|
||||||
@ -390,32 +392,32 @@
|
|||||||
"cancel": "Abbruch",
|
"cancel": "Abbruch",
|
||||||
"Comment": {
|
"Comment": {
|
||||||
"disable": {
|
"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"
|
"title": "Sperre den Kommentar abschließend"
|
||||||
},
|
},
|
||||||
"enable": {
|
"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"
|
"title": "Entsperre den Kommentar abschließend"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Post": {
|
"Post": {
|
||||||
"disable": {
|
"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"
|
"title": "Sperre den Beitrag abschließend"
|
||||||
},
|
},
|
||||||
"enable": {
|
"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"
|
"title": "Entsperre den Beitrag abschließend"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"submit": "Bestätige Entscheidung",
|
"submit": "Bestätige Entscheidung",
|
||||||
"User": {
|
"User": {
|
||||||
"disable": {
|
"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"
|
"title": "Sperre den Benutzer abschließend"
|
||||||
},
|
},
|
||||||
"enable": {
|
"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"
|
"title": "Entsperre den Benutzer abschließend"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -459,9 +461,9 @@
|
|||||||
"pageLink": "Alle Benachrichtigungen",
|
"pageLink": "Alle Benachrichtigungen",
|
||||||
"post": "Beitrag",
|
"post": "Beitrag",
|
||||||
"reason": {
|
"reason": {
|
||||||
"commented_on_post": "Hat deinen Beitrag kommentiert …",
|
"commented_on_post": "Hat Deinen Beitrag kommentiert …",
|
||||||
"mentioned_in_comment": "Hat dich in einem Kommentar erwähnt …",
|
"mentioned_in_comment": "Hat Dich in einem Kommentar erwähnt …",
|
||||||
"mentioned_in_post": "Hat dich in einem Beitrag erwähnt …"
|
"mentioned_in_post": "Hat Dich in einem Beitrag erwähnt …"
|
||||||
},
|
},
|
||||||
"title": "Benachrichtigungen",
|
"title": "Benachrichtigungen",
|
||||||
"user": "Benutzer"
|
"user": "Benutzer"
|
||||||
@ -470,20 +472,20 @@
|
|||||||
"comment": {
|
"comment": {
|
||||||
"reply": "Antworten",
|
"reply": "Antworten",
|
||||||
"submit": "Kommentiere",
|
"submit": "Kommentiere",
|
||||||
"submitted": "Kommentar gesendet!",
|
"submitted": "Kommentar gesendet",
|
||||||
"updated": "Änderungen gespeichert"
|
"updated": "Änderungen gespeichert"
|
||||||
},
|
},
|
||||||
"edited": "bearbeitet",
|
"edited": "bearbeitet",
|
||||||
"menu": {
|
"menu": {
|
||||||
"delete": "Beitrag löschen",
|
"delete": "Beitrag löschen",
|
||||||
"edit": "Beitrag bearbeiten",
|
"edit": "Beitrag bearbeiten",
|
||||||
"pin": "Post festpinnen",
|
"pin": "Beitrag anheften",
|
||||||
"pinnedSuccessfully": "Post erfolgreich festgepinnt!",
|
"pinnedSuccessfully": "Beitrag erfolgreich angeheftet!",
|
||||||
"unpin": "Post nicht mehr festpinnen",
|
"unpin": "Beitrag loslösen",
|
||||||
"unpinnedSuccessfully": "Post erfolgreich nicht mehr festgepinnt!"
|
"unpinnedSuccessfully": "Angehefteten Beitrag erfolgreich losgelöst!"
|
||||||
},
|
},
|
||||||
"moreInfo": {
|
"moreInfo": {
|
||||||
"description": "Hier findest du weitere Infos zum Thema.",
|
"description": "Hier findest Du weitere Infos zum Thema.",
|
||||||
"name": "Mehr Info",
|
"name": "Mehr Info",
|
||||||
"title": "Mehr Informationen",
|
"title": "Mehr Informationen",
|
||||||
"titleOfCategoriesSection": "Kategorien",
|
"titleOfCategoriesSection": "Kategorien",
|
||||||
@ -529,22 +531,22 @@
|
|||||||
"release": {
|
"release": {
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"comment": {
|
"comment": {
|
||||||
"error": "Den Kommentar hast du schon gemeldet!",
|
"error": "Den Kommentar hast Du schon gemeldet!",
|
||||||
"message": "Bist du sicher, dass du den Kommentar \"<b>{name}</b>\" freigeben möchtest?",
|
"message": "Bist Du sicher, dass Du den Kommentar „<b>{name}</b>“ freigeben möchtest?",
|
||||||
"title": "Kommentar freigeben",
|
"title": "Kommentar freigeben",
|
||||||
"type": "Kommentar"
|
"type": "Kommentar"
|
||||||
},
|
},
|
||||||
"contribution": {
|
"contribution": {
|
||||||
"error": "Den Beitrag hast du schon gemeldet!",
|
"error": "Den Beitrag hast Du schon gemeldet!",
|
||||||
"message": "Bist du sicher, dass du den Beitrag \"<b>{name}</b>\" freigeben möchtest?",
|
"message": "Bist Du sicher, dass Du den Beitrag „<b>{name}</b>“ freigeben möchtest?",
|
||||||
"title": "Beitrag freigeben",
|
"title": "Beitrag freigeben",
|
||||||
"type": "Beitrag"
|
"type": "Beitrag"
|
||||||
},
|
},
|
||||||
"submit": "freigeben",
|
"submit": "freigeben",
|
||||||
"success": "Erfolgreich freigegeben!",
|
"success": "Erfolgreich freigegeben!",
|
||||||
"user": {
|
"user": {
|
||||||
"error": "Den User hast du schon gemeldet!",
|
"error": "Den Benutzer hast Du schon gemeldet!",
|
||||||
"message": "Bist du sicher, dass du den Nutzer \"<b>{name}</b>\" freigeben möchtest?",
|
"message": "Bist Du sicher, dass Du den Nutzer „<b>{name}</b>“ freigeben möchtest?",
|
||||||
"title": "Nutzer freigeben",
|
"title": "Nutzer freigeben",
|
||||||
"type": "Nutzer"
|
"type": "Nutzer"
|
||||||
}
|
}
|
||||||
@ -571,16 +573,16 @@
|
|||||||
"advert_products_services_commercial": "Bewerben von Produkten und Dienstleistungen mit kommerzieller Absicht.",
|
"advert_products_services_commercial": "Bewerben von Produkten und Dienstleistungen mit kommerzieller Absicht.",
|
||||||
"criminal_behavior_violation_german_law": "Strafbares Verhalten bzw. Verstoß gegen deutsches Recht.",
|
"criminal_behavior_violation_german_law": "Strafbares Verhalten bzw. Verstoß gegen deutsches Recht.",
|
||||||
"discrimination_etc": "Diskriminierende Beiträge, Kommentare, Äußerungen oder Beleidigungen.",
|
"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.",
|
"glorific_trivia_of_cruel_inhuman_acts": "Verherrlichung oder Verharmlosung grausamer oder unmenschlicher Gewalttätigkeiten.",
|
||||||
"intentional_intimidation_stalking_persecution": "Absichtliche Einschüchterung, Stalking oder Verfolgung.",
|
"intentional_intimidation_stalking_persecution": "Absichtliche Einschüchterung, Stalking oder Verfolgung.",
|
||||||
"other": "Andere …",
|
"other": "Andere …",
|
||||||
"pornographic_content_links": "Das Posten oder Verlinken eindeutig pornografischen Materials."
|
"pornographic_content_links": "Das Senden oder Verlinken eindeutig pornografischen Materials."
|
||||||
},
|
},
|
||||||
"placeholder": "Kategorie …"
|
"placeholder": "Kategorie …"
|
||||||
},
|
},
|
||||||
"description": {
|
"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 …"
|
"placeholder": "Zusätzliche Information …"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -610,19 +612,19 @@
|
|||||||
"slug": "Alias",
|
"slug": "Alias",
|
||||||
"unblock": "Entsperren"
|
"unblock": "Entsperren"
|
||||||
},
|
},
|
||||||
"empty": "Bislang hast du niemanden blockiert.",
|
"empty": "Bislang hast Du niemanden blockiert.",
|
||||||
"explanation": {
|
"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-disabled": "Du kannst den Beitrag derzeit nicht kommentieren.",
|
||||||
"commenting-explanation": "Dafür kann es mehrere Gründe geben, bitte schau in unsere ",
|
"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:",
|
"intro": "Wenn ein anderer Benutzer durch Dich blockiert wurde, dann passiert Folgendes:",
|
||||||
"notifications": "Von dir blockierte Personen erhalten keine Benachrichtigungen mehr, wenn sie in deinen Beiträgen erwähnt werden.",
|
"notifications": "Von Dir blockierte Benutzer werden keine Benachrichtigungen mehr erhalten, falls sie in Deinen Beiträgen erwähnt werden.",
|
||||||
"their-perspective": "Die blockierte Person kann deine Beiträge nicht mehr kommentieren",
|
"their-perspective": "Umgekehrt das gleiche: Die blockierte Person bekommt auch in ihren Benachrichtigungen Deine Beiträge nicht mehr zu sehen.",
|
||||||
"your-perspective": "Du kannst keine Beiträge der blockierten Person mehr kommentieren."
|
"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.",
|
"how-to": "Du kannst andere Benutzer auf deren Profilseite über das Inhaltsmenü blockieren.",
|
||||||
"name": "Blocked users",
|
"name": "Blockierte Benutzer",
|
||||||
"unblock": "Nutzer entsperren",
|
"unblock": "Blockierten Nutzer freigeben",
|
||||||
"unblocked": "{name} ist wieder entsperrt"
|
"unblocked": "{name} ist wieder entsperrt"
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
@ -636,47 +638,47 @@
|
|||||||
},
|
},
|
||||||
"deleteUserAccount": {
|
"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.",
|
"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",
|
"commentedCount": "Meine {count} Kommentare löschen",
|
||||||
"contributionsCount": "Meine {count} Beiträge löschen",
|
"contributionsCount": "Meine {count} Beiträge löschen",
|
||||||
"name": "Benutzerkonto 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!"
|
"success": "Konto erfolgreich gelöscht!"
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"name": "Daten herunterladen"
|
"name": "Daten herunterladen"
|
||||||
},
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"change-successful": "Deine E-Mail Adresse wurde erfolgreich geändert.",
|
"change-successful": "Deine E-Mail-Adresse wurde erfolgreich geändert.",
|
||||||
"labelEmail": "E-Mail Adresse ändern",
|
"labelEmail": "E-Mail-Adresse ändern",
|
||||||
"labelNewEmail": "Neue E-Mail Adresse",
|
"labelNewEmail": "Neue E-Mail-Adresse",
|
||||||
"labelNonce": "Bestätigungscode eingeben",
|
"labelNonce": "Bestätigungscode eingeben",
|
||||||
"name": "Deine E-Mail",
|
"name": "Deine E-Mail",
|
||||||
"submitted": "Eine E-Mail zur Bestätigung deiner Adresse wurde an <b>{email}</b> gesendet.",
|
"submitted": "Eine E-Mail zur Bestätigung Deiner Adresse wurde an <b>{email}</b> gesendet.",
|
||||||
"success": "Eine neue E-Mail Addresse wurde registriert.",
|
"success": "Eine neue E-Mail-Adresse wurde registriert.",
|
||||||
"validation": {
|
"validation": {
|
||||||
"same-email": "Das ist deine aktuelle E-Mail Addresse"
|
"same-email": "Das ist Deine aktuelle E-Mail-Adresse"
|
||||||
},
|
},
|
||||||
"verification-error": {
|
"verification-error": {
|
||||||
"explanation": "Das kann verschiedene Ursachen haben:",
|
"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": {
|
"reason": {
|
||||||
"invalid-nonce": "Ist der Bestätigungscode falsch?",
|
"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"
|
"support": "Wenn das Problem weiterhin besteht, kontaktiere uns gerne per E-Mail an"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"embeds": {
|
"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",
|
"name": "Drittanbieter",
|
||||||
"status": {
|
"status": {
|
||||||
"change": {
|
"change": {
|
||||||
"allow": "Na klar",
|
"allow": "Na klar",
|
||||||
"deny": "Lieber nicht",
|
"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": {
|
"disabled": {
|
||||||
"off": "zunächst nicht angezeigt",
|
"off": "zunächst nicht angezeigt",
|
||||||
"on": "sofort angezeigt"
|
"on": "sofort angezeigt"
|
||||||
@ -722,10 +724,10 @@
|
|||||||
"label-new-password": "Dein neues Passwort",
|
"label-new-password": "Dein neues Passwort",
|
||||||
"label-new-password-confirm": "Bestätige Dein neues Passwort",
|
"label-new-password-confirm": "Bestätige Dein neues Passwort",
|
||||||
"label-old-password": "Dein altes Passwort",
|
"label-old-password": "Dein altes Passwort",
|
||||||
"message-new-password-confirm-required": "Bestätige dein neues Passwort",
|
"message-new-password-confirm-required": "Bestätige Dein neues Passwort",
|
||||||
"message-new-password-missmatch": "Gebe das gleiche Passwort nochmals ein",
|
"message-new-password-missmatch": "Gib dasselbe Passwort nochmals ein",
|
||||||
"message-new-password-required": "Gebe ein neues Passwort ein",
|
"message-new-password-required": "Gib ein neues Passwort ein",
|
||||||
"message-old-password-required": "Gebe dein altes Passwort ein",
|
"message-old-password-required": "Gib Dein altes Passwort ein",
|
||||||
"passwordSecurity": "Passwortsicherheit",
|
"passwordSecurity": "Passwortsicherheit",
|
||||||
"passwordStrength0": "Sehr unsicheres Passwort",
|
"passwordStrength0": "Sehr unsicheres Passwort",
|
||||||
"passwordStrength1": "Unsicheres Passwort",
|
"passwordStrength1": "Unsicheres Passwort",
|
||||||
@ -737,12 +739,12 @@
|
|||||||
"name": "Sicherheit"
|
"name": "Sicherheit"
|
||||||
},
|
},
|
||||||
"social-media": {
|
"social-media": {
|
||||||
"name": "Soziale Medien",
|
"name": "Soziale Netzwerke",
|
||||||
"placeholder": "Deine Social-Media URL",
|
"placeholder": "Deine Webadresse des Sozialen Netzwerkes",
|
||||||
"requireUnique": "Dieser Link existiert bereits",
|
"requireUnique": "Dieser Link existiert bereits",
|
||||||
"submit": "Link hinzufügen",
|
"submit": "Link hinzufügen",
|
||||||
"successAdd": "Social-Media hinzugefügt. Profil aktualisiert!",
|
"successAdd": "Soziales Netzwerk hinzugefügt. Profil wurde aktualisiert!",
|
||||||
"successDelete": "Social-Media gelöscht. Profil aktualisiert!"
|
"successDelete": "Soziales Netzwerk entfernt. Profil wurde aktualisiert!"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"slug": {
|
"slug": {
|
||||||
@ -767,7 +769,7 @@
|
|||||||
"imprint": "Impressum",
|
"imprint": "Impressum",
|
||||||
"made": "Mit ❤ gemacht",
|
"made": "Mit ❤ gemacht",
|
||||||
"register": "Registernummer",
|
"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)",
|
"taxident": "Umsatzsteuer-Identifikationsnummer gemäß § 27 a Umsatzsteuergesetz (Deutschland)",
|
||||||
"termsAndConditions": "Nutzungsbedingungen",
|
"termsAndConditions": "Nutzungsbedingungen",
|
||||||
"thanks": "Danke!",
|
"thanks": "Danke!",
|
||||||
@ -788,11 +790,11 @@
|
|||||||
"termsAndConditions": {
|
"termsAndConditions": {
|
||||||
"addition": {
|
"addition": {
|
||||||
"description": "<a href=\"https://human-connection.org/veranstaltungen/\" target=\"_blank\" > https://human-connection.org/veranstaltungen/ </a>",
|
"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!",
|
"agree": "Ich stimme zu!",
|
||||||
"code-of-conduct": {
|
"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"
|
"title": "Verhaltenscodex"
|
||||||
},
|
},
|
||||||
"errors-and-feedback": {
|
"errors-and-feedback": {
|
||||||
@ -800,7 +802,7 @@
|
|||||||
"title": "Fehler und Rückmeldungen"
|
"title": "Fehler und Rückmeldungen"
|
||||||
},
|
},
|
||||||
"help-and-questions": {
|
"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"
|
"title": "Hilfe und Fragen"
|
||||||
},
|
},
|
||||||
"moderation": {
|
"moderation": {
|
||||||
@ -809,11 +811,15 @@
|
|||||||
},
|
},
|
||||||
"newTermsAndConditions": "Neue Nutzungsbedingungen",
|
"newTermsAndConditions": "Neue Nutzungsbedingungen",
|
||||||
"no-commercial-use": {
|
"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"
|
"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": {
|
"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"
|
"title": "Datenschutz"
|
||||||
},
|
},
|
||||||
"terms-of-service": {
|
"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.",
|
"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.",
|
"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": {
|
"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.",
|
"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"
|
"title": "Nutzung und Lizenz"
|
||||||
@ -830,7 +836,7 @@
|
|||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"avatar": {
|
"avatar": {
|
||||||
"submitted": "Upload erfolgreich"
|
"submitted": "Erfolgreich hochgeladen!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -217,6 +217,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"contribution": {
|
"contribution": {
|
||||||
|
"amount-comments": "{amount} comments",
|
||||||
|
"amount-shouts": "{amount} recommendations",
|
||||||
"categories": {
|
"categories": {
|
||||||
"infoSelectedNoOfMaxCategories": "{chosen} of {max} categories selected"
|
"infoSelectedNoOfMaxCategories": "{chosen} of {max} categories selected"
|
||||||
},
|
},
|
||||||
@ -636,11 +638,11 @@
|
|||||||
},
|
},
|
||||||
"deleteUserAccount": {
|
"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.",
|
"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",
|
"commentedCount": "Delete my {count} comments",
|
||||||
"contributionsCount": "Delete my {count} posts",
|
"contributionsCount": "Delete my {count} posts",
|
||||||
"name": "Delete user account",
|
"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!"
|
"success": "Account successfully deleted!"
|
||||||
},
|
},
|
||||||
"download": {
|
"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.",
|
"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"
|
"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": {
|
"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.",
|
"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"
|
"title": "Privacy Statement"
|
||||||
|
|||||||
@ -636,11 +636,11 @@
|
|||||||
},
|
},
|
||||||
"deleteUserAccount": {
|
"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.",
|
"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",
|
"commentedCount": "Eliminar mis {count} comentarios",
|
||||||
"contributionsCount": "Eliminar mis {count} contribuciones",
|
"contributionsCount": "Eliminar mis {count} contribuciones",
|
||||||
"name": "Borrar datos",
|
"name": "Eliminar cuenta de usuario",
|
||||||
"pleaseConfirm": "<b class='is-danger'> ¡Acción destructiva! </b> Escriba <b> {confirm} </b> para confirmar",
|
"pleaseConfirm": "¡Acción destructiva! Escriba “{confirm}” para confirmar.",
|
||||||
"success": "¡Cuenta eliminada con éxito!"
|
"success": "¡Cuenta eliminada con éxito!"
|
||||||
},
|
},
|
||||||
"download": {
|
"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.",
|
"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"
|
"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": {
|
"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.",
|
"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"
|
"title": "Protección de datos"
|
||||||
|
|||||||
@ -152,7 +152,7 @@
|
|||||||
"project": "Projet ::: Projets",
|
"project": "Projet ::: Projets",
|
||||||
"reportContent": "Signaler",
|
"reportContent": "Signaler",
|
||||||
"shout": "Partage ::: Partages",
|
"shout": "Partage ::: Partages",
|
||||||
"tag": "Tag ::: Tags",
|
"tag": "Tag ::: Tags",
|
||||||
"takeAction": "Passer à l'action",
|
"takeAction": "Passer à l'action",
|
||||||
"user": "Utilisateur ::: Utilisateurs",
|
"user": "Utilisateur ::: Utilisateurs",
|
||||||
"validations": {
|
"validations": {
|
||||||
@ -622,11 +622,11 @@
|
|||||||
},
|
},
|
||||||
"deleteUserAccount": {
|
"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.",
|
"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",
|
"commentedCount": "Supprimer mes {count} commentaires",
|
||||||
"contributionsCount": "Supprimer mes {count} postes",
|
"contributionsCount": "Supprimer mes {count} postes",
|
||||||
"name": "Effacer les données",
|
"name": "Supprimer un compte utilisateur",
|
||||||
"pleaseConfirm": "<b class='is-danger'> Action destructive! </b> Saisissez <b> {confirm} </b> pour confirmer",
|
"pleaseConfirm": "Action destructive! Saisissez “{confirm}” pour confirmer.",
|
||||||
"success": "Compte supprimer avec succès!"
|
"success": "Compte supprimer avec succès!"
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
|
|||||||
@ -551,11 +551,11 @@
|
|||||||
},
|
},
|
||||||
"deleteUserAccount": {
|
"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!",
|
"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",
|
"commentedCount": "Cancella i miei {count} commenti",
|
||||||
"contributionsCount": "Cancellare i miei {count} messaggi",
|
"contributionsCount": "Cancellare i miei {count} messaggi",
|
||||||
"name": "Cancellare l'account utente",
|
"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!"
|
"success": "Account eliminato con successo!"
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
|
|||||||
@ -295,11 +295,11 @@
|
|||||||
},
|
},
|
||||||
"deleteUserAccount": {
|
"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.",
|
"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",
|
"commentedCount": "Usuń {count} moich komentarzy",
|
||||||
"contributionsCount": "Usuń {count} moich postów",
|
"contributionsCount": "Usuń {count} moich postów",
|
||||||
"name": "Usuń dane",
|
"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"
|
"success": "Konto zostało usunięte"
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
|
|||||||
@ -557,11 +557,11 @@
|
|||||||
},
|
},
|
||||||
"deleteUserAccount": {
|
"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.",
|
"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",
|
"commentedCount": "Deletar meus {count} comentários",
|
||||||
"contributionsCount": "Deletar minhas {count} publicações",
|
"contributionsCount": "Deletar minhas {count} publicações",
|
||||||
"name": "Deletar dados",
|
"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!"
|
"success": "Conta eliminada com sucesso!"
|
||||||
},
|
},
|
||||||
"download": {
|
"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