test: specs not ok, lint is ok

This commit is contained in:
ogerly 2020-01-22 11:31:23 +01:00
commit 21fa211e0c
131 changed files with 6931 additions and 5652 deletions

1
.github/stale.yml vendored
View File

@ -6,6 +6,7 @@ daysUntilClose: 30
exemptLabels: exemptLabels:
- pinned - pinned
- security - security
- bounty
# Label to use when marking an issue as stale # Label to use when marking an issue as stale
staleLabel: stale staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable # Comment to post when marking an issue as stale. Set to `false` to disable

View File

@ -8,5 +8,4 @@
} }
], ],
"editor.formatOnSave": false, "editor.formatOnSave": false,
"eslint.autoFixOnSave": true
} }

View File

@ -4,6 +4,176 @@ 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.2.1](https://github.com/Human-Connection/Human-Connection/compare/v0.2.0...v0.2.1)
> 10 January 2020
- 🍰 Search For Users [`#2262`](https://github.com/Human-Connection/Human-Connection/pull/2262)
- Use node LTS in production [`#2713`](https://github.com/Human-Connection/Human-Connection/pull/2713)
- build(deps): bump apollo-server from 2.9.15 to 2.9.16 in /backend [`#2718`](https://github.com/Human-Connection/Human-Connection/pull/2718)
- build(deps): bump neo4j-graphql-js from 2.11.4 to 2.11.5 in /backend [`#2715`](https://github.com/Human-Connection/Human-Connection/pull/2715)
- build(deps-dev): bump apollo-server-testing from 2.9.15 to 2.9.16 in /backend [`#2720`](https://github.com/Human-Connection/Human-Connection/pull/2720)
- build(deps): bump @hapi/joi from 17.0.0 to 17.0.2 in /backend [`#2719`](https://github.com/Human-Connection/Human-Connection/pull/2719)
- build(deps): bump apollo-server-express from 2.9.15 to 2.9.16 in /backend [`#2717`](https://github.com/Human-Connection/Human-Connection/pull/2717)
- build(deps): bump metascraper-url from 5.8.13 to 5.9.5 in /backend [`#2716`](https://github.com/Human-Connection/Human-Connection/pull/2716)
- build(deps): bump date-fns from 2.8.1 to 2.9.0 in /backend [`#2706`](https://github.com/Human-Connection/Human-Connection/pull/2706)
- build(deps): bump metascraper-lang from 5.8.13 to 5.9.5 in /backend [`#2703`](https://github.com/Human-Connection/Human-Connection/pull/2703)
- build(deps): bump date-fns from 2.8.1 to 2.9.0 in /webapp [`#2711`](https://github.com/Human-Connection/Human-Connection/pull/2711)
- build(deps): bump metascraper-logo from 5.8.13 to 5.9.5 in /backend [`#2697`](https://github.com/Human-Connection/Human-Connection/pull/2697)
- build(deps): bump metascraper-title from 5.8.13 to 5.9.5 in /backend [`#2694`](https://github.com/Human-Connection/Human-Connection/pull/2694)
- build(deps): bump metascraper-description from 5.8.15 to 5.9.5 in /backend [`#2690`](https://github.com/Human-Connection/Human-Connection/pull/2690)
- build(deps): bump node from 13.5.0-alpine to 13.6.0-alpine in /webapp [`#2708`](https://github.com/Human-Connection/Human-Connection/pull/2708)
- build(deps): bump @sentry/node from 5.10.2 to 5.11.0 in /backend [`#2709`](https://github.com/Human-Connection/Human-Connection/pull/2709)
- build(deps): bump metascraper-audio from 5.8.13 to 5.9.5 in /backend [`#2707`](https://github.com/Human-Connection/Human-Connection/pull/2707)
- build(deps): bump metascraper-image from 5.9.4 to 5.9.5 in /backend [`#2705`](https://github.com/Human-Connection/Human-Connection/pull/2705)
- build(deps): bump metascraper-youtube from 5.8.13 to 5.9.5 in /backend [`#2704`](https://github.com/Human-Connection/Human-Connection/pull/2704)
- build(deps-dev): bump date-fns from 2.8.1 to 2.9.0 [`#2702`](https://github.com/Human-Connection/Human-Connection/pull/2702)
- build(deps): bump metascraper-soundcloud from 5.9.0 to 5.9.5 in /backend [`#2693`](https://github.com/Human-Connection/Human-Connection/pull/2693)
- build(deps): bump metascraper-date from 5.8.13 to 5.9.5 in /backend [`#2698`](https://github.com/Human-Connection/Human-Connection/pull/2698)
- build(deps): bump neo4j-graphql-js from 2.11.3 to 2.11.4 in /backend [`#2696`](https://github.com/Human-Connection/Human-Connection/pull/2696)
- build(deps): bump metascraper-video from 5.8.13 to 5.9.5 in /backend [`#2695`](https://github.com/Human-Connection/Human-Connection/pull/2695)
- build(deps): bump metascraper-publisher from 5.8.13 to 5.9.5 in /backend [`#2692`](https://github.com/Human-Connection/Human-Connection/pull/2692)
- build(deps): bump metascraper-author from 5.8.13 to 5.9.5 in /backend [`#2691`](https://github.com/Human-Connection/Human-Connection/pull/2691)
- build(deps): bump metascraper from 5.9.4 to 5.9.5 in /backend [`#2689`](https://github.com/Human-Connection/Human-Connection/pull/2689)
- Changes Text For SignUp [`#2678`](https://github.com/Human-Connection/Human-Connection/pull/2678)
- Update de.json [`#2655`](https://github.com/Human-Connection/Human-Connection/pull/2655)
- build(deps): bump neode from 0.3.6 to 0.3.7 in /backend [`#2682`](https://github.com/Human-Connection/Human-Connection/pull/2682)
- Update neo4j-driver [`#2546`](https://github.com/Human-Connection/Human-Connection/pull/2546)
- build(deps): bump merge-graphql-schemas from 1.7.5 to 1.7.6 in /backend [`#2681`](https://github.com/Human-Connection/Human-Connection/pull/2681)
- build(deps): bump neo4j-graphql-js from 2.11.2 to 2.11.3 in /backend [`#2680`](https://github.com/Human-Connection/Human-Connection/pull/2680)
- build(deps-dev): bump neode from 0.3.6 to 0.3.7 [`#2679`](https://github.com/Human-Connection/Human-Connection/pull/2679)
- Parse xss before extracting mentions/hashtags [`#2674`](https://github.com/Human-Connection/Human-Connection/pull/2674)
- build(deps): bump metascraper-logo from 5.8.12 to 5.8.13 in /backend [`#2672`](https://github.com/Human-Connection/Human-Connection/pull/2672)
- build(deps): bump metascraper from 5.9.0 to 5.9.4 in /backend [`#2668`](https://github.com/Human-Connection/Human-Connection/pull/2668)
- build(deps-dev): bump eslint-plugin-jest from 23.2.0 to 23.3.0 in /webapp [`#2671`](https://github.com/Human-Connection/Human-Connection/pull/2671)
- build(deps-dev): bump css-loader from 3.4.0 to 3.4.1 in /webapp [`#2669`](https://github.com/Human-Connection/Human-Connection/pull/2669)
- build(deps): bump metascraper-image from 5.8.13 to 5.9.4 in /backend [`#2670`](https://github.com/Human-Connection/Human-Connection/pull/2670)
- build(deps-dev): bump apollo-server-testing from 2.9.14 to 2.9.15 in /backend [`#2667`](https://github.com/Human-Connection/Human-Connection/pull/2667)
- build(deps): bump neo4j-graphql-js from 2.11.0 to 2.11.2 in /backend [`#2666`](https://github.com/Human-Connection/Human-Connection/pull/2666)
- build(deps): bump metascraper-title from 5.8.12 to 5.8.13 in /backend [`#2665`](https://github.com/Human-Connection/Human-Connection/pull/2665)
- build(deps): bump @hapi/joi from 16.1.8 to 17.0.0 in /backend [`#2664`](https://github.com/Human-Connection/Human-Connection/pull/2664)
- build(deps-dev): bump cypress-file-upload from 3.5.1 to 3.5.3 [`#2663`](https://github.com/Human-Connection/Human-Connection/pull/2663)
- build(deps-dev): bump eslint-plugin-jest from 23.1.1 to 23.3.0 in /backend [`#2662`](https://github.com/Human-Connection/Human-Connection/pull/2662)
- build(deps-dev): bump eslint-config-prettier from 6.7.0 to 6.9.0 in /webapp [`#2632`](https://github.com/Human-Connection/Human-Connection/pull/2632)
- build(deps-dev): bump slug from 2.0.0 to 2.1.0 [`#2647`](https://github.com/Human-Connection/Human-Connection/pull/2647)
- build(deps): bump merge-graphql-schemas from 1.7.3 to 1.7.5 in /backend [`#2648`](https://github.com/Human-Connection/Human-Connection/pull/2648)
- build(deps): bump metascraper-url from 5.8.12 to 5.8.13 in /backend [`#2637`](https://github.com/Human-Connection/Human-Connection/pull/2637)
- build(deps): bump metascraper-publisher from 5.8.12 to 5.8.13 in /backend [`#2636`](https://github.com/Human-Connection/Human-Connection/pull/2636)
- build(deps-dev): bump eslint-plugin-jest from 23.1.1 to 23.2.0 in /webapp [`#2642`](https://github.com/Human-Connection/Human-Connection/pull/2642)
- build(deps): bump graphql-shield from 7.0.5 to 7.0.7 in /backend [`#2649`](https://github.com/Human-Connection/Human-Connection/pull/2649)
- build(deps-dev): bump eslint-plugin-node from 10.0.0 to 11.0.0 in /backend [`#2650`](https://github.com/Human-Connection/Human-Connection/pull/2650)
- build(deps-dev): bump eslint-config-prettier from 6.7.0 to 6.9.0 in /backend [`#2651`](https://github.com/Human-Connection/Human-Connection/pull/2651)
- build(deps): bump slug from 2.0.0 to 2.1.0 in /backend [`#2652`](https://github.com/Human-Connection/Human-Connection/pull/2652)
- build(deps): bump metascraper from 5.8.12 to 5.9.0 in /backend [`#2654`](https://github.com/Human-Connection/Human-Connection/pull/2654)
- build(deps): bump metascraper-description from 5.8.12 to 5.8.15 in /backend [`#2653`](https://github.com/Human-Connection/Human-Connection/pull/2653)
- build(deps): bump metascraper-author from 5.8.12 to 5.8.13 in /backend [`#2616`](https://github.com/Human-Connection/Human-Connection/pull/2616)
- build(deps): bump metascraper-lang from 5.8.12 to 5.8.13 in /backend [`#2618`](https://github.com/Human-Connection/Human-Connection/pull/2618)
- build(deps): bump apollo-server from 2.9.13 to 2.9.15 in /backend [`#2634`](https://github.com/Human-Connection/Human-Connection/pull/2634)
- build(deps): bump metascraper-soundcloud from 5.8.15 to 5.9.0 in /backend [`#2638`](https://github.com/Human-Connection/Human-Connection/pull/2638)
- build(deps): bump metascraper-video from 5.8.12 to 5.8.13 in /backend [`#2639`](https://github.com/Human-Connection/Human-Connection/pull/2639)
- build(deps): bump mustache from 3.2.0 to 3.2.1 in /backend [`#2640`](https://github.com/Human-Connection/Human-Connection/pull/2640)
- build(deps): bump slug from 1.1.0 to 2.0.0 in /backend [`#2641`](https://github.com/Human-Connection/Human-Connection/pull/2641)
- build(deps-dev): bump @vue/cli-shared-utils from 4.1.1 to 4.1.2 in /webapp [`#2643`](https://github.com/Human-Connection/Human-Connection/pull/2643)
- build(deps-dev): bump eslint-plugin-vue from 6.1.1 to 6.1.2 in /webapp [`#2644`](https://github.com/Human-Connection/Human-Connection/pull/2644)
- build(deps-dev): bump eslint-plugin-node from 10.0.0 to 11.0.0 in /webapp [`#2645`](https://github.com/Human-Connection/Human-Connection/pull/2645)
- build(deps-dev): bump @babel/preset-env from 7.7.6 to 7.7.7 in /webapp [`#2579`](https://github.com/Human-Connection/Human-Connection/pull/2579)
- build(deps): bump metascraper-soundcloud from 5.8.12 to 5.8.15 in /backend [`#2630`](https://github.com/Human-Connection/Human-Connection/pull/2630)
- build(deps-dev): bump eslint from 6.7.2 to 6.8.0 in /webapp [`#2617`](https://github.com/Human-Connection/Human-Connection/pull/2617)
- build(deps): bump metascraper-date from 5.8.12 to 5.8.13 in /backend [`#2615`](https://github.com/Human-Connection/Human-Connection/pull/2615)
- build(deps): bump metascraper-image from 5.8.12 to 5.8.13 in /backend [`#2614`](https://github.com/Human-Connection/Human-Connection/pull/2614)
- build(deps): bump metascraper-youtube from 5.8.12 to 5.8.13 in /backend [`#2612`](https://github.com/Human-Connection/Human-Connection/pull/2612)
- build(deps): bump metascraper-audio from 5.8.12 to 5.8.13 in /backend [`#2610`](https://github.com/Human-Connection/Human-Connection/pull/2610)
- 🍰 Added Language Tag For Posts [`#2627`](https://github.com/Human-Connection/Human-Connection/pull/2627)
- build(deps-dev): bump cypress-plugin-retries from 1.5.0 to 1.5.2 [`#2609`](https://github.com/Human-Connection/Human-Connection/pull/2609)
- build(deps-dev): bump eslint from 6.7.2 to 6.8.0 in /backend [`#2613`](https://github.com/Human-Connection/Human-Connection/pull/2613)
- remove accidently created ru.json in wrong place [`#2606`](https://github.com/Human-Connection/Human-Connection/pull/2606)
- build(deps): bump neo4j from 3.5.13-enterprise to 3.5.14-enterprise in /neo4j [`#2620`](https://github.com/Human-Connection/Human-Connection/pull/2620)
- Fixes 2603 [`#2619`](https://github.com/Human-Connection/Human-Connection/pull/2619)
- build(deps-dev): bump @babel/core from 7.7.5 to 7.7.7 in /webapp [`#2581`](https://github.com/Human-Connection/Human-Connection/pull/2581)
- build(deps-dev): bump slug from 1.1.0 to 2.0.0 [`#2621`](https://github.com/Human-Connection/Human-Connection/pull/2621)
- build(deps): [security] bump handlebars from 4.1.2 to 4.5.3 in /webapp [`#2624`](https://github.com/Human-Connection/Human-Connection/pull/2624)
- build(deps): [security] bump handlebars from 4.1.2 to 4.5.3 in /backend [`#2625`](https://github.com/Human-Connection/Human-Connection/pull/2625)
- build(deps-dev): bump cypress from 3.8.0 to 3.8.1 [`#2626`](https://github.com/Human-Connection/Human-Connection/pull/2626)
- build(deps-dev): bump eslint-plugin-vue from 6.0.1 to 6.1.1 in /webapp [`#2633`](https://github.com/Human-Connection/Human-Connection/pull/2633)
- build(deps-dev): bump @babel/register from 7.7.4 to 7.7.7 [`#2571`](https://github.com/Human-Connection/Human-Connection/pull/2571)
- build(deps): bump neo4j-graphql-js from 2.10.2 to 2.11.0 in /backend [`#2600`](https://github.com/Human-Connection/Human-Connection/pull/2600)
- build(deps-dev): bump @babel/core from 7.7.5 to 7.7.7 in /backend [`#2590`](https://github.com/Human-Connection/Human-Connection/pull/2590)
- build(deps): bump metascraper-url from 5.8.7 to 5.8.12 in /backend [`#2599`](https://github.com/Human-Connection/Human-Connection/pull/2599)
- build(deps): bump metascraper-lang from 5.8.10 to 5.8.12 in /backend [`#2598`](https://github.com/Human-Connection/Human-Connection/pull/2598)
- build(deps): bump metascraper-audio from 5.8.10 to 5.8.12 in /backend [`#2596`](https://github.com/Human-Connection/Human-Connection/pull/2596)
- build(deps): bump node from 13.4.0-alpine to 13.5.0-alpine in /webapp [`#2595`](https://github.com/Human-Connection/Human-Connection/pull/2595)
- build(deps-dev): bump storybook-design-token from 0.4.1 to 0.5.0 in /webapp [`#2594`](https://github.com/Human-Connection/Human-Connection/pull/2594)
- build(deps): bump graphql-shield from 7.0.4 to 7.0.5 in /backend [`#2593`](https://github.com/Human-Connection/Human-Connection/pull/2593)
- build(deps): bump metascraper-publisher from 5.8.7 to 5.8.12 in /backend [`#2592`](https://github.com/Human-Connection/Human-Connection/pull/2592)
- build(deps-dev): bump @babel/preset-env from 7.7.6 to 7.7.7 in /backend [`#2568`](https://github.com/Human-Connection/Human-Connection/pull/2568)
- Fix imageAspectRatio set to null UpdatePost [`#2588`](https://github.com/Human-Connection/Human-Connection/pull/2588)
- Update to version 0.2.0 [`#2584`](https://github.com/Human-Connection/Human-Connection/pull/2584)
- fixes #2659 [`#2659`](https://github.com/Human-Connection/Human-Connection/issues/2659)
- build(deps-dev): bump storybook-design-token in /webapp [`88d39c4`](https://github.com/Human-Connection/Human-Connection/commit/88d39c4a427cb86527b06201f3f5e96d53ac09a0)
- Specs for Searches [`bc3aa51`](https://github.com/Human-Connection/Human-Connection/commit/bc3aa519d0e7a6e0242ecd37d611fd1a3df385d0)
- build(deps): bump apollo-server-express in /backend [`84df7b5`](https://github.com/Human-Connection/Human-Connection/commit/84df7b5a0a4845ab44d19946d877aef79691d38e)
#### [v0.2.0](https://github.com/Human-Connection/Human-Connection/compare/v0.1.13...v0.2.0)
> 19 December 2019
- build(deps): bump metascraper-image from 5.8.10 to 5.8.12 in /backend [`#2556`](https://github.com/Human-Connection/Human-Connection/pull/2556)
- build(deps-dev): bump @babel/core from 7.7.5 to 7.7.7 [`#2569`](https://github.com/Human-Connection/Human-Connection/pull/2569)
- build(deps-dev): bump @babel/cli from 7.7.5 to 7.7.7 in /backend [`#2576`](https://github.com/Human-Connection/Human-Connection/pull/2576)
- Lokalise: Translations update [`#2563`](https://github.com/Human-Connection/Human-Connection/pull/2563)
- build(deps-dev): bump style-resources-loader from 1.3.2 to 1.3.3 in /webapp [`#2580`](https://github.com/Human-Connection/Human-Connection/pull/2580)
- build(deps): bump node from 13.3.0-alpine to 13.4.0-alpine in /webapp [`#2577`](https://github.com/Human-Connection/Human-Connection/pull/2577)
- build(deps): bump metascraper-title from 5.8.10 to 5.8.12 in /backend [`#2575`](https://github.com/Human-Connection/Human-Connection/pull/2575)
- build(deps-dev): bump apollo-server-testing from 2.9.13 to 2.9.14 in /backend [`#2574`](https://github.com/Human-Connection/Human-Connection/pull/2574)
- build(deps): bump mustache from 3.1.0 to 3.2.0 in /backend [`#2572`](https://github.com/Human-Connection/Human-Connection/pull/2572)
- Blur Images [`#2351`](https://github.com/Human-Connection/Human-Connection/pull/2351)
- build(deps-dev): bump @babel/node from 7.7.4 to 7.7.7 in /backend [`#2570`](https://github.com/Human-Connection/Human-Connection/pull/2570)
- build(deps-dev): bump @babel/preset-env from 7.7.6 to 7.7.7 [`#2567`](https://github.com/Human-Connection/Human-Connection/pull/2567)
- build(deps): bump metascraper-description from 5.8.10 to 5.8.12 in /backend [`#2566`](https://github.com/Human-Connection/Human-Connection/pull/2566)
- Add back layout changes/update db_manipulation [`#2544`](https://github.com/Human-Connection/Human-Connection/pull/2544)
- build(deps): bump metascraper-soundcloud from 5.8.10 to 5.8.12 in /backend [`#2560`](https://github.com/Human-Connection/Human-Connection/pull/2560)
- build(deps): bump metascraper-author from 5.8.7 to 5.8.12 in /backend [`#2559`](https://github.com/Human-Connection/Human-Connection/pull/2559)
- build(deps): bump metascraper from 5.8.9 to 5.8.12 in /backend [`#2558`](https://github.com/Human-Connection/Human-Connection/pull/2558)
- build(deps): bump metascraper-youtube from 5.8.9 to 5.8.12 in /backend [`#2547`](https://github.com/Human-Connection/Human-Connection/pull/2547)
- build(deps): bump metascraper-video from 5.8.10 to 5.8.12 in /backend [`#2557`](https://github.com/Human-Connection/Human-Connection/pull/2557)
- build(deps): bump metascraper-date from 5.8.7 to 5.8.12 in /backend [`#2555`](https://github.com/Human-Connection/Human-Connection/pull/2555)
- build(deps): bump metascraper-logo from 5.8.10 to 5.8.12 in /backend [`#2554`](https://github.com/Human-Connection/Human-Connection/pull/2554)
- build(deps): bump apollo-server-express from 2.9.13 to 2.9.14 in /backend [`#2551`](https://github.com/Human-Connection/Human-Connection/pull/2551)
- build(deps-dev): bump css-loader from 3.3.2 to 3.4.0 in /webapp [`#2550`](https://github.com/Human-Connection/Human-Connection/pull/2550)
- build(deps-dev): bump cypress-cucumber-preprocessor from 1.18.0 to 1.19.0 [`#2548`](https://github.com/Human-Connection/Human-Connection/pull/2548)
- Lokalise: Translations update [`#2545`](https://github.com/Human-Connection/Human-Connection/pull/2545)
- build(deps): bump metascraper-youtube from 5.8.9 to 5.8.10 in /backend [`#2522`](https://github.com/Human-Connection/Human-Connection/pull/2522)
- build(deps): bump metascraper-title from 5.8.7 to 5.8.10 in /backend [`#2525`](https://github.com/Human-Connection/Human-Connection/pull/2525)
- build(deps): bump metascraper-lang from 5.8.9 to 5.8.10 in /backend [`#2531`](https://github.com/Human-Connection/Human-Connection/pull/2531)
- build(deps): bump tiptap-extensions from 1.28.5 to 1.28.6 in /webapp [`#2535`](https://github.com/Human-Connection/Human-Connection/pull/2535)
- Fix maintenance service/LocaleSwitch import [`#2542`](https://github.com/Human-Connection/Human-Connection/pull/2542)
- build(deps): bump apollo-client from 2.6.4 to 2.6.8 in /webapp [`#2523`](https://github.com/Human-Connection/Human-Connection/pull/2523)
- build(deps): bump stack-utils from 1.0.2 to 2.0.1 in /webapp [`#2521`](https://github.com/Human-Connection/Human-Connection/pull/2521)
- build(deps): bump metascraper-soundcloud from 5.8.9 to 5.8.10 in /backend [`#2520`](https://github.com/Human-Connection/Human-Connection/pull/2520)
- Update neode [`#2539`](https://github.com/Human-Connection/Human-Connection/pull/2539)
- build(deps-dev): bump eslint-plugin-prettier from 3.1.1 to 3.1.2 in /webapp [`#2519`](https://github.com/Human-Connection/Human-Connection/pull/2519)
- build(deps): bump apollo-cache-inmemory from 1.6.3 to 1.6.5 in /webapp [`#2527`](https://github.com/Human-Connection/Human-Connection/pull/2527)
- build(deps): bump neo4j-graphql-js from 2.10.1 to 2.10.2 in /backend [`#2530`](https://github.com/Human-Connection/Human-Connection/pull/2530)
- build(deps): bump metascraper-image from 5.8.7 to 5.8.10 in /backend [`#2532`](https://github.com/Human-Connection/Human-Connection/pull/2532)
- build(deps): bump apollo-cache-inmemory from 1.6.3 to 1.6.5 in /backend [`#2534`](https://github.com/Human-Connection/Human-Connection/pull/2534)
- build(deps): bump metascraper-video from 5.8.9 to 5.8.10 in /backend [`#2536`](https://github.com/Human-Connection/Human-Connection/pull/2536)
- build(deps): bump tiptap from 1.26.5 to 1.26.6 in /webapp [`#2537`](https://github.com/Human-Connection/Human-Connection/pull/2537)
- build(deps-dev): bump vue-loader from 15.7.2 to 15.8.3 in /webapp [`#2538`](https://github.com/Human-Connection/Human-Connection/pull/2538)
- Refactor: content menu [`#2512`](https://github.com/Human-Connection/Human-Connection/pull/2512)
- build(deps): bump metascraper-audio from 5.8.7 to 5.8.10 in /backend [`#2524`](https://github.com/Human-Connection/Human-Connection/pull/2524)
- build(deps): bump metascraper-description from 5.8.7 to 5.8.10 in /backend [`#2518`](https://github.com/Human-Connection/Human-Connection/pull/2518)
- build(deps-dev): bump eslint-plugin-prettier from 3.1.1 to 3.1.2 in /backend [`#2517`](https://github.com/Human-Connection/Human-Connection/pull/2517)
- build(deps): bump apollo-client from 2.6.4 to 2.6.8 in /backend [`#2516`](https://github.com/Human-Connection/Human-Connection/pull/2516)
- build(deps): bump metascraper-logo from 5.8.7 to 5.8.10 in /backend [`#2515`](https://github.com/Human-Connection/Human-Connection/pull/2515)
- Fix duplicate fragment `user` issue [`#2511`](https://github.com/Human-Connection/Human-Connection/pull/2511)
- fix: editor not visible in server-side-rendering [`#2513`](https://github.com/Human-Connection/Human-Connection/pull/2513)
- Update it.json [`#2507`](https://github.com/Human-Connection/Human-Connection/pull/2507)
- Fix: User.name is not non-nullable [`#2510`](https://github.com/Human-Connection/Human-Connection/pull/2510)
- Update to version 0.1.13 [`#2506`](https://github.com/Human-Connection/Human-Connection/pull/2506)
- Lokalise: update of webapp/locales/ru.json [`b70ff73`](https://github.com/Human-Connection/Human-Connection/commit/b70ff73bba98d28494c55ed12161288b1efa1516)
- Separate concerns in components [`d74d207`](https://github.com/Human-Connection/Human-Connection/commit/d74d2072ba41af6170d79d7dc2e24f9ebab15771)
- Fix failing component tests [`b79c292`](https://github.com/Human-Connection/Human-Connection/commit/b79c292ef4f76b307131566c24d849c2e35c8089)
#### [v0.1.13](https://github.com/Human-Connection/Human-Connection/compare/v0.1.12...v0.1.13) #### [v0.1.13](https://github.com/Human-Connection/Human-Connection/compare/v0.1.12...v0.1.13)
> 13 December 2019 > 13 December 2019
@ -30,8 +200,8 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- build(deps): bump cookie-universal-nuxt from 2.0.19 to 2.1.0 in /webapp [`#2490`](https://github.com/Human-Connection/Human-Connection/pull/2490) - build(deps): bump cookie-universal-nuxt from 2.0.19 to 2.1.0 in /webapp [`#2490`](https://github.com/Human-Connection/Human-Connection/pull/2490)
- Update to version 0.1.12 [`#2483`](https://github.com/Human-Connection/Human-Connection/pull/2483) - Update to version 0.1.12 [`#2483`](https://github.com/Human-Connection/Human-Connection/pull/2483)
- Lokalise: update of locale/ru.json [`60b3035`](https://github.com/Human-Connection/Human-Connection/commit/60b3035a3d475cb481130c6fe94f2901711a4053) - Lokalise: update of locale/ru.json [`60b3035`](https://github.com/Human-Connection/Human-Connection/commit/60b3035a3d475cb481130c6fe94f2901711a4053)
- Fix search by adding result id [`ebc5cf3`](https://github.com/Human-Connection/Human-Connection/commit/ebc5cf392d92acf3a9e22c8967d02ea2cf6fd7fb)
- Write test/refactor tests/resolvers/middleware [`d375ebe`](https://github.com/Human-Connection/Human-Connection/commit/d375ebe7d90e3251b17f59ffba8fb1470923ebe8) - Write test/refactor tests/resolvers/middleware [`d375ebe`](https://github.com/Human-Connection/Human-Connection/commit/d375ebe7d90e3251b17f59ffba8fb1470923ebe8)
- Fix this annoying bug with a tested helper [`e24d803`](https://github.com/Human-Connection/Human-Connection/commit/e24d8035b13040dc29f5f9cb033de8c1a401ac34)
#### [v0.1.12](https://github.com/Human-Connection/Human-Connection/compare/v0.1.10...v0.1.12) #### [v0.1.12](https://github.com/Human-Connection/Human-Connection/compare/v0.1.10...v0.1.12)
@ -196,9 +366,9 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- fix #2329: Normalize email on login in the backend [`#2329`](https://github.com/Human-Connection/Human-Connection/issues/2329) - fix #2329: Normalize email on login in the backend [`#2329`](https://github.com/Human-Connection/Human-Connection/issues/2329)
- Fix #2294 [`#2294`](https://github.com/Human-Connection/Human-Connection/issues/2294) - Fix #2294 [`#2294`](https://github.com/Human-Connection/Human-Connection/issues/2294)
- Merge pull request #2078 from Human-Connection/fix-2042-back-link [`#2042`](https://github.com/Human-Connection/Human-Connection/issues/2042) - Merge pull request #2078 from Human-Connection/fix-2042-back-link [`#2042`](https://github.com/Human-Connection/Human-Connection/issues/2042)
- Tell github-linguists to ignore snapshots [`978347b`](https://github.com/Human-Connection/Human-Connection/commit/978347ba7b5a6aa1bc915ada972ffffa2816d37c) - Move components to components/features [`2357028`](https://github.com/Human-Connection/Human-Connection/commit/235702867d97b44dac37f8059f9194e23ba7f47d)
- Lokalise: update of webapp/locales/ru.json [`906e851`](https://github.com/Human-Connection/Human-Connection/commit/906e8518bf060134150187fb1574ac50ffd502f6) - Basic Search Is Working For Users And Posts [`72e4d0a`](https://github.com/Human-Connection/Human-Connection/commit/72e4d0abbcb9abab07f3fd12876453eb1de5da4c)
- Lokalise: update of webapp/locales/ru.json [`3e52ee0`](https://github.com/Human-Connection/Human-Connection/commit/3e52ee090c88c357b796895370d126f8bb5529f0) - Add missing unit tests/refactor code [`b364065`](https://github.com/Human-Connection/Human-Connection/commit/b3640659bb608cc34edc6f2aca350f07dd2b9ce6)
#### [v0.1.10](https://github.com/Human-Connection/Human-Connection/compare/v0.1.9...v0.1.10) #### [v0.1.10](https://github.com/Human-Connection/Human-Connection/compare/v0.1.9...v0.1.10)

View File

@ -1 +1 @@
0.1.13 0.2.1

View File

@ -1,4 +1,4 @@
FROM node:12.12.0-alpine as base FROM node:lts-alpine as base
LABEL Description="Backend of the Social Network Human-Connection.org" Vendor="Human Connection gGmbH" Version="0.0.1" Maintainer="Human Connection gGmbH (developer@human-connection.org)" LABEL Description="Backend of the Social Network Human-Connection.org" Vendor="Human Connection gGmbH" Version="0.0.1" Maintainer="Human Connection gGmbH (developer@human-connection.org)"
EXPOSE 4000 EXPOSE 4000

View File

@ -32,20 +32,20 @@
] ]
}, },
"dependencies": { "dependencies": {
"@hapi/joi": "^16.1.8", "@hapi/joi": "^17.0.2",
"@sentry/node": "^5.10.2", "@sentry/node": "^5.11.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.9.13", "apollo-server": "~2.9.16",
"apollo-server-express": "^2.9.14", "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": "~6.0.3", "cross-env": "~6.0.3",
"date-fns": "2.8.1", "date-fns": "2.9.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",
@ -55,41 +55,41 @@
"graphql-iso-date": "~3.6.1", "graphql-iso-date": "~3.6.1",
"graphql-middleware": "~4.0.2", "graphql-middleware": "~4.0.2",
"graphql-middleware-sentry": "^3.2.1", "graphql-middleware-sentry": "^3.2.1",
"graphql-shield": "~7.0.4", "graphql-shield": "~7.0.7",
"graphql-tag": "~2.10.1", "graphql-tag": "~2.10.1",
"helmet": "~3.21.2", "helmet": "~3.21.2",
"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.3", "merge-graphql-schemas": "^1.7.6",
"metascraper": "^5.8.12", "metascraper": "^5.10.2",
"metascraper-audio": "^5.8.10", "metascraper-audio": "^5.9.5",
"metascraper-author": "^5.8.12", "metascraper-author": "^5.9.5",
"metascraper-clearbit-logo": "^5.3.0", "metascraper-clearbit-logo": "^5.3.0",
"metascraper-date": "^5.8.12", "metascraper-date": "^5.10.3",
"metascraper-description": "^5.8.10", "metascraper-description": "^5.9.5",
"metascraper-image": "^5.8.10", "metascraper-image": "^5.9.5",
"metascraper-lang": "^5.8.10", "metascraper-lang": "^5.9.5",
"metascraper-lang-detector": "^4.10.2", "metascraper-lang-detector": "^4.10.2",
"metascraper-logo": "^5.8.12", "metascraper-logo": "^5.9.5",
"metascraper-publisher": "^5.8.7", "metascraper-publisher": "^5.10.3",
"metascraper-soundcloud": "^5.8.12", "metascraper-soundcloud": "^5.9.5",
"metascraper-title": "^5.8.10", "metascraper-title": "^5.10.3",
"metascraper-url": "^5.8.7", "metascraper-url": "^5.9.5",
"metascraper-video": "^5.8.12", "metascraper-video": "^5.9.5",
"metascraper-youtube": "^5.8.12", "metascraper-youtube": "^5.9.5",
"minimatch": "^3.0.4", "minimatch": "^3.0.4",
"mustache": "^3.1.0", "mustache": "^3.2.1",
"neo4j-driver": "~1.7.6", "neo4j-driver": "^4.0.1",
"neo4j-graphql-js": "^2.10.2", "neo4j-graphql-js": "^2.11.5",
"neode": "^0.3.6", "neode": "^0.3.7",
"node-fetch": "~2.6.0", "node-fetch": "~2.6.0",
"nodemailer": "^6.4.2", "nodemailer": "^6.4.2",
"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.0", "request": "~2.88.0",
"sanitize-html": "~1.20.1", "sanitize-html": "~1.20.1",
"slug": "~1.1.0", "slug": "~2.1.0",
"trunc-html": "~1.1.2", "trunc-html": "~1.1.2",
"uuid": "~3.3.3", "uuid": "~3.3.3",
"validator": "^12.1.0", "validator": "^12.1.0",
@ -97,24 +97,24 @@
"xregexp": "^4.2.4" "xregexp": "^4.2.4"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "~7.7.5", "@babel/cli": "~7.8.3",
"@babel/core": "~7.7.5", "@babel/core": "~7.8.3",
"@babel/node": "~7.7.4", "@babel/node": "~7.8.3",
"@babel/plugin-proposal-throw-expressions": "^7.7.4", "@babel/plugin-proposal-throw-expressions": "^7.8.3",
"@babel/preset-env": "~7.7.6", "@babel/preset-env": "~7.8.3",
"@babel/register": "~7.7.0", "@babel/register": "~7.8.3",
"apollo-server-testing": "~2.9.13", "apollo-server-testing": "~2.9.16",
"babel-core": "~7.0.0-0", "babel-core": "~7.0.0-0",
"babel-eslint": "~10.0.3", "babel-eslint": "~10.0.3",
"babel-jest": "~24.9.0", "babel-jest": "~24.9.0",
"chai": "~4.2.0", "chai": "~4.2.0",
"cucumber": "~6.0.5", "cucumber": "~6.0.5",
"eslint": "~6.7.2", "eslint": "~6.8.0",
"eslint-config-prettier": "~6.7.0", "eslint-config-prettier": "~6.9.0",
"eslint-config-standard": "~14.1.0", "eslint-config-standard": "~14.1.0",
"eslint-plugin-import": "~2.19.1", "eslint-plugin-import": "~2.20.0",
"eslint-plugin-jest": "~23.1.1", "eslint-plugin-jest": "~23.6.0",
"eslint-plugin-node": "~10.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",
"eslint-plugin-standard": "~4.0.1", "eslint-plugin-standard": "~4.0.1",

View File

@ -1,4 +1,4 @@
import { v1 as neo4j } from 'neo4j-driver' import neo4j from 'neo4j-driver'
import CONFIG from './../config' import CONFIG from './../config'
import Neode from 'neode' import Neode from 'neode'
import models from '../models' import models from '../models'

View File

@ -7,7 +7,6 @@ import sluggify from './sluggifyMiddleware'
import excerpt from './excerptMiddleware' import excerpt from './excerptMiddleware'
import xss from './xssMiddleware' import xss from './xssMiddleware'
import permissions from './permissionsMiddleware' import permissions from './permissionsMiddleware'
import user from './user/userMiddleware'
import includedFields from './includedFieldsMiddleware' import includedFields from './includedFieldsMiddleware'
import orderBy from './orderByMiddleware' import orderBy from './orderByMiddleware'
import validation from './validation/validationMiddleware' import validation from './validation/validationMiddleware'
@ -18,25 +17,25 @@ import sentry from './sentryMiddleware'
export default schema => { export default schema => {
const middlewares = { const middlewares = {
permissions,
sentry, sentry,
permissions,
xss,
activityPub, activityPub,
validation, validation,
sluggify, sluggify,
excerpt, excerpt,
email,
notifications, notifications,
hashtags, hashtags,
xss,
softDelete, softDelete,
user,
includedFields, includedFields,
orderBy, orderBy,
email,
} }
let order = [ let order = [
'sentry', 'sentry',
'permissions', 'permissions',
'xss',
// 'activityPub', disabled temporarily // 'activityPub', disabled temporarily
'validation', 'validation',
'sluggify', 'sluggify',
@ -44,9 +43,7 @@ export default schema => {
'email', 'email',
'notifications', 'notifications',
'hashtags', 'hashtags',
'xss',
'softDelete', 'softDelete',
'user',
'includedFields', 'includedFields',
'orderBy', 'orderBy',
] ]

View File

@ -85,6 +85,8 @@ export default shield(
Query: { Query: {
'*': deny, '*': deny,
findPosts: allow, findPosts: allow,
findUsers: allow,
findResources: allow,
embed: allow, embed: allow,
Category: allow, Category: allow,
Tag: allow, Tag: allow,

View File

@ -1,16 +0,0 @@
import createOrUpdateLocations from '../nodes/locations'
export default {
Mutation: {
SignupVerification: async (resolve, root, args, context, info) => {
const result = await resolve(root, args, context, info)
await createOrUpdateLocations(result.id, args.locationName, context.driver)
return result
},
UpdateUser: async (resolve, root, args, context, info) => {
const result = await resolve(root, args, context, info)
await createOrUpdateLocations(args.id, args.locationName, context.driver)
return result
},
},
}

View File

@ -39,5 +39,6 @@ module.exports = {
default: () => new Date().toISOString(), default: () => new Date().toISOString(),
}, },
language: { type: 'string', allow: [null] }, language: { type: 'string', allow: [null] },
imageBlurred: { type: 'boolean', default: false },
imageAspectRatio: { type: 'float', default: 1.0 }, imageAspectRatio: { type: 'float', default: 1.0 },
} }

View File

@ -3,8 +3,8 @@ const debugCypher = Debug('human-connection:neo4j:cypher')
const debugStats = Debug('human-connection:neo4j:stats') const debugStats = Debug('human-connection:neo4j:stats')
export default function log(response) { export default function log(response) {
const { statement, counters, resultConsumedAfter, resultAvailableAfter } = response.summary const { counters, resultConsumedAfter, resultAvailableAfter, query } = response.summary
const { text, parameters } = statement const { text, parameters } = query
debugCypher('%s', text) debugCypher('%s', text)
debugCypher('%o', parameters) debugCypher('%o', parameters)
debugStats('%o', counters) debugStats('%o', counters)

View File

@ -0,0 +1,25 @@
import { getBlockedUsers, getBlockedByUsers } from '../users.js'
import { mergeWith, isArray } from 'lodash'
export const filterForBlockedUsers = async (params, context) => {
if (!context.user) return params
const [blockedUsers, blockedByUsers] = await Promise.all([
getBlockedUsers(context),
getBlockedByUsers(context),
])
const blockedUsersIds = [...blockedByUsers.map(b => b.id), ...blockedUsers.map(b => b.id)]
if (!blockedUsersIds.length) return params
params.filter = mergeWith(
params.filter,
{
author_not: { id_in: blockedUsersIds },
},
(objValue, srcValue) => {
if (isArray(objValue)) {
return objValue.concat(srcValue)
}
},
)
return params
}

View File

@ -1,33 +1,10 @@
import uuid from 'uuid/v4' import uuid from 'uuid/v4'
import { neo4jgraphql } from 'neo4j-graphql-js' import { neo4jgraphql } from 'neo4j-graphql-js'
import { isEmpty } from 'lodash'
import fileUpload from './fileUpload' import fileUpload from './fileUpload'
import { getBlockedUsers, getBlockedByUsers } from './users.js'
import { mergeWith, isArray, isEmpty } from 'lodash'
import { UserInputError } from 'apollo-server' import { UserInputError } from 'apollo-server'
import Resolver from './helpers/Resolver' import Resolver from './helpers/Resolver'
import { filterForBlockedUsers } from './helpers/filterForBlockedUsers'
const filterForBlockedUsers = async (params, context) => {
if (!context.user) return params
const [blockedUsers, blockedByUsers] = await Promise.all([
getBlockedUsers(context),
getBlockedByUsers(context),
])
const badIds = [...blockedByUsers.map(b => b.id), ...blockedUsers.map(b => b.id)]
if (!badIds.length) return params
params.filter = mergeWith(
params.filter,
{
author_not: { id_in: badIds },
},
(objValue, srcValue) => {
if (isArray(objValue)) {
return objValue.concat(srcValue)
}
},
)
return params
}
const maintainPinnedPosts = params => { const maintainPinnedPosts = params => {
const pinnedPostFilter = { pinned: true } const pinnedPostFilter = { pinned: true }
@ -341,6 +318,7 @@ export default {
'language', 'language',
'pinnedAt', 'pinnedAt',
'pinned', 'pinned',
'imageBlurred',
'imageAspectRatio', 'imageAspectRatio',
], ],
hasMany: { hasMany: {

View File

@ -5,6 +5,7 @@ import encryptPassword from '../../helpers/encryptPassword'
import generateNonce from './helpers/generateNonce' import generateNonce from './helpers/generateNonce'
import existingEmailAddress from './helpers/existingEmailAddress' import existingEmailAddress from './helpers/existingEmailAddress'
import normalizeEmail from './helpers/normalizeEmail' import normalizeEmail from './helpers/normalizeEmail'
import createOrUpdateLocations from './users/location'
const neode = getNeode() const neode = getNeode()
@ -22,7 +23,9 @@ export default {
throw new UserInputError(e.message) throw new UserInputError(e.message)
} }
}, },
SignupVerification: async (_parent, args) => { SignupVerification: async (_parent, args, context) => {
const { driver } = context
const session = driver.session()
const { termsAndConditionsAgreedVersion } = args const { termsAndConditionsAgreedVersion } = args
const regEx = new RegExp(/^[0-9]+\.[0-9]+\.[0-9]+$/g) const regEx = new RegExp(/^[0-9]+\.[0-9]+\.[0-9]+$/g)
if (!regEx.test(termsAndConditionsAgreedVersion)) { if (!regEx.test(termsAndConditionsAgreedVersion)) {
@ -51,11 +54,14 @@ export default {
emailAddress.relateTo(user, 'belongsTo'), emailAddress.relateTo(user, 'belongsTo'),
emailAddress.update({ verifiedAt: new Date().toISOString() }), emailAddress.update({ verifiedAt: new Date().toISOString() }),
]) ])
await createOrUpdateLocations(args.id, args.locationName, session)
return user.toJson() return user.toJson()
} catch (e) { } catch (e) {
if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed') if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')
throw new UserInputError('User with this slug already exists!') throw new UserInputError('User with this slug already exists!')
throw new UserInputError(e.message) throw new UserInputError(e.message)
} finally {
session.close()
} }
}, },
}, },

View File

@ -0,0 +1,74 @@
import log from './helpers/databaseLogger'
export default {
Query: {
findResources: async (_parent, args, context, _resolveInfo) => {
const { query, limit } = args
const { id: thisUserId } = context.user
// see http://lucene.apache.org/core/8_3_1/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package.description
const myQuery = query
.replace(/\s+/g, ' ')
.replace(/[[@#:*~\\$|^\]?/"'(){}+?!,.-;]/g, '')
.split(' ')
.map(s => (s.toLowerCase().match(/^(not|and|or)$/) ? '"' + s + '"' : s + '*'))
.join(' ')
const postCypher = `
CALL db.index.fulltext.queryNodes('post_fulltext_search', $query)
YIELD node as resource, score
MATCH (resource)<-[:WROTE]-(author:User)
WHERE score >= 0.5
AND NOT (
author.deleted = true OR author.disabled = true
OR resource.deleted = true OR resource.disabled = true
OR (:User { id: $thisUserId })-[:BLOCKED]-(author)
)
WITH resource, author,
[(resource)<-[:COMMENTS]-(comment:Comment) | comment] as comments,
[(resource)<-[:SHOUTED]-(user:User) | user] as shouter
RETURN resource {
.*,
__typename: labels(resource)[0],
author: properties(author),
commentsCount: toString(size(comments)),
shoutedCount: toString(size(shouter))
}
LIMIT $limit
`
const userCypher = `
CALL db.index.fulltext.queryNodes('user_fulltext_search', $query)
YIELD node as resource, score
MATCH (resource)
WHERE score >= 0.5
AND NOT (resource.deleted = true OR resource.disabled = true
OR (:User { id: $thisUserId })-[:BLOCKED]-(resource))
RETURN resource {.*, __typename: labels(resource)[0]}
LIMIT $limit
`
const session = context.driver.session()
const searchResultPromise = session.readTransaction(async transaction => {
const postTransactionResponse = transaction.run(postCypher, {
query: myQuery,
limit,
thisUserId,
})
const userTransactionResponse = transaction.run(userCypher, {
query: myQuery,
limit,
thisUserId,
})
return Promise.all([postTransactionResponse, userTransactionResponse])
})
try {
const [postResults, userResults] = await searchResultPromise
log(postResults)
log(userResults)
return [...postResults.records, ...userResults.records].map(r => r.get('resource'))
} finally {
session.close()
}
},
},
}

View File

@ -4,6 +4,7 @@ import { getNeode } from '../../bootstrap/neo4j'
import { UserInputError, ForbiddenError } from 'apollo-server' import { UserInputError, ForbiddenError } from 'apollo-server'
import Resolver from './helpers/Resolver' import Resolver from './helpers/Resolver'
import log from './helpers/databaseLogger' import log from './helpers/databaseLogger'
import createOrUpdateLocations from './users/location'
const neode = getNeode() const neode = getNeode()
@ -127,6 +128,7 @@ export default {
}) })
try { try {
const [user] = await writeTxResultPromise const [user] = await writeTxResultPromise
await createOrUpdateLocations(params.id, params.locationName, session)
return user return user
} catch (error) { } catch (error) {
throw new UserInputError(error.message) throw new UserInputError(error.message)

View File

@ -2,8 +2,8 @@ import request from 'request'
import { UserInputError } from 'apollo-server' import { UserInputError } from 'apollo-server'
import isEmpty from 'lodash/isEmpty' import isEmpty from 'lodash/isEmpty'
import Debug from 'debug' import Debug from 'debug'
import asyncForEach from '../../helpers/asyncForEach' import asyncForEach from '../../../helpers/asyncForEach'
import CONFIG from './../../config' import CONFIG from '../../../config'
const debug = Debug('human-connection:location') const debug = Debug('human-connection:location')
@ -57,16 +57,12 @@ const createLocation = async (session, mapboxData) => {
} }
mutation += ' RETURN l.id' mutation += ' RETURN l.id'
try { await session.writeTransaction(transaction => {
await session.writeTransaction(transaction => { return transaction.run(mutation, data)
return transaction.run(mutation, data) })
})
} finally {
session.close()
}
} }
const createOrUpdateLocations = async (userId, locationName, driver) => { const createOrUpdateLocations = async (userId, locationName, session) => {
if (isEmpty(locationName)) { if (isEmpty(locationName)) {
return return
} }
@ -99,7 +95,6 @@ const createOrUpdateLocations = async (userId, locationName, driver) => {
throw new UserInputError('locationName is invalid') throw new UserInputError('locationName is invalid')
} }
const session = driver.session()
if (data.place_type.length > 1) { if (data.place_type.length > 1) {
data.id = 'region.' + data.id.split('.')[1] data.id = 'region.' + data.id.split('.')[1]
} }
@ -110,44 +105,36 @@ const createOrUpdateLocations = async (userId, locationName, driver) => {
if (data.context) { if (data.context) {
await asyncForEach(data.context, async ctx => { await asyncForEach(data.context, async ctx => {
await createLocation(session, ctx) await createLocation(session, ctx)
try { await session.writeTransaction(transaction => {
await session.writeTransaction(transaction => { return transaction.run(
return transaction.run( `
`
MATCH (parent:Location {id: $parentId}), (child:Location {id: $childId}) MATCH (parent:Location {id: $parentId}), (child:Location {id: $childId})
MERGE (child)<-[:IS_IN]-(parent) MERGE (child)<-[:IS_IN]-(parent)
RETURN child.id, parent.id RETURN child.id, parent.id
`, `,
{ {
parentId: parent.id, parentId: parent.id,
childId: ctx.id, childId: ctx.id,
}, },
) )
}) })
parent = ctx parent = ctx
} finally {
session.close()
}
}) })
} }
// delete all current locations from user and add new location // delete all current locations from user and add new location
try { await session.writeTransaction(transaction => {
await session.writeTransaction(transaction => { return transaction.run(
return transaction.run( `
`
MATCH (user:User {id: $userId})-[relationship:IS_IN]->(location:Location) MATCH (user:User {id: $userId})-[relationship:IS_IN]->(location:Location)
DETACH DELETE relationship DETACH DELETE relationship
WITH user WITH user
MATCH (location:Location {id: $locationId}) MATCH (location:Location {id: $locationId})
MERGE (user)-[:IS_IN]->(location) MERGE (user)-[:IS_IN]->(location)
RETURN location.id, user.id RETURN location.id, user.id
`, `,
{ userId: userId, locationId: data.id }, { userId: userId, locationId: data.id },
) )
}) })
} finally {
session.close()
}
} }
export default createOrUpdateLocations export default createOrUpdateLocations

View File

@ -1,8 +1,8 @@
import { gql } from '../../helpers/jest' import { gql } from '../../../helpers/jest'
import Factory from '../../seed/factories' import Factory from '../../../seed/factories'
import { getNeode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../../bootstrap/neo4j'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import createServer from '../../server' import createServer from '../../../server'
const factory = Factory() const factory = Factory()
const neode = getNeode() const neode = getNeode()

View File

@ -1,23 +1,3 @@
type Query {
isLoggedIn: Boolean!
# Get the currently logged in User based on the given JWT Token
currentUser: User
findPosts(query: String!, limit: Int = 10, filter: _PostFilter): [Post]!
@cypher(
statement: """
CALL db.index.fulltext.queryNodes('full_text_search', $query)
YIELD node as post, score
MATCH (post)<-[:WROTE]-(user:User)
WHERE score >= 0.2
AND NOT user.deleted = true AND NOT user.disabled = true
AND NOT post.deleted = true AND NOT post.disabled = true
AND NOT user.id in COALESCE($filter.author_not.id_in, [])
RETURN post
LIMIT $limit
"""
)
}
type Mutation { type Mutation {
# Get a JWT Token for the given Email and password # Get a JWT Token for the given Email and password
login(email: String!, password: String!): String! login(email: String!, password: String!): String!

View File

@ -82,6 +82,7 @@ input _PostFilter {
emotions_none: _PostEMOTEDFilter emotions_none: _PostEMOTEDFilter
emotions_single: _PostEMOTEDFilter emotions_single: _PostEMOTEDFilter
emotions_every: _PostEMOTEDFilter emotions_every: _PostEMOTEDFilter
imageBlurred: Boolean
} }
enum _PostOrdering { enum _PostOrdering {
@ -127,6 +128,7 @@ type Post {
createdAt: String createdAt: String
updatedAt: String updatedAt: String
language: String language: String
imageBlurred: Boolean
pinnedAt: String @cypher( pinnedAt: String @cypher(
statement: "MATCH (this)<-[pinned:PINNED]-(:User) WHERE NOT this.deleted = true AND NOT this.disabled = true RETURN pinned.createdAt" statement: "MATCH (this)<-[pinned:PINNED]-(:User) WHERE NOT this.deleted = true AND NOT this.disabled = true RETURN pinned.createdAt"
) )
@ -140,7 +142,6 @@ type Post {
LIMIT 10 LIMIT 10
""" """
) )
tags: [Tag]! @relation(name: "TAGGED", direction: "OUT") tags: [Tag]! @relation(name: "TAGGED", direction: "OUT")
categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT") categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
@ -183,6 +184,7 @@ type Mutation {
language: String language: String
categoryIds: [ID] categoryIds: [ID]
contentExcerpt: String contentExcerpt: String
imageBlurred: Boolean
imageAspectRatio: Float imageAspectRatio: Float
): Post ): Post
UpdatePost( UpdatePost(
@ -196,6 +198,7 @@ type Mutation {
visibility: Visibility visibility: Visibility
language: String language: String
categoryIds: [ID] categoryIds: [ID]
imageBlurred: Boolean
imageAspectRatio: Float imageAspectRatio: Float
): Post ): Post
DeletePost(id: ID!): Post DeletePost(id: ID!): Post
@ -217,6 +220,7 @@ type Query {
createdAt: String createdAt: String
updatedAt: String updatedAt: String
language: String language: String
imageBlurred: Boolean
first: Int first: Int
offset: Int offset: Int
orderBy: [_PostOrdering] orderBy: [_PostOrdering]
@ -226,4 +230,18 @@ type Query {
PostsEmotionsCountByEmotion(postId: ID!, data: _EMOTEDInput!): Int! PostsEmotionsCountByEmotion(postId: ID!, data: _EMOTEDInput!): Int!
PostsEmotionsByCurrentUser(postId: ID!): [String] PostsEmotionsByCurrentUser(postId: ID!): [String]
profilePagePosts(filter: _PostFilter, first: Int, offset: Int, orderBy: [_PostOrdering]): [Post] profilePagePosts(filter: _PostFilter, first: Int, offset: Int, orderBy: [_PostOrdering]): [Post]
findPosts(query: String!, limit: Int = 10, filter: _PostFilter): [Post]!
@cypher(
statement: """
CALL db.index.fulltext.queryNodes('post_fulltext_search', $query)
YIELD node as post, score
MATCH (post)<-[:WROTE]-(user:User)
WHERE score >= 0.2
AND NOT user.deleted = true AND NOT user.disabled = true
AND NOT post.deleted = true AND NOT post.disabled = true
AND NOT user.id in COALESCE($filter.author_not.id_in, [])
RETURN post
LIMIT $limit
"""
)
} }

View File

@ -0,0 +1,5 @@
union SearchResult = Post | User
type Query {
findResources(query: String!, limit: Int = 5): [SearchResult]!
}

View File

@ -161,7 +161,20 @@ type Query {
): [User] ): [User]
blockedUsers: [User] blockedUsers: [User]
isLoggedIn: Boolean!
currentUser: User currentUser: User
findUsers(query: String!,limit: Int = 10, filter: _UserFilter): [User]!
@cypher(
statement: """
CALL db.index.fulltext.queryNodes('user_fulltext_search', $query)
YIELD node as post, score
MATCH (user)
WHERE score >= 0.2
AND NOT user.deleted = true AND NOT user.disabled = true
RETURN user
LIMIT $limit
"""
)
} }
type Mutation { type Mutation {
@ -179,8 +192,7 @@ type Mutation {
termsAndConditionsAgreedAt: String termsAndConditionsAgreedAt: String
allowEmbedIframes: Boolean allowEmbedIframes: Boolean
showShoutsPublicly: Boolean showShoutsPublicly: Boolean
locale: String
locale: String
): User ): User
DeleteUser(id: ID!, resource: [Deletable]): User DeleteUser(id: ID!, resource: [Deletable]): User

View File

@ -19,6 +19,7 @@ export default function create() {
visibility: 'public', visibility: 'public',
deleted: false, deleted: false,
categoryIds: [], categoryIds: [],
imageBlurred: false,
imageAspectRatio: 1.333, imageAspectRatio: 1.333,
} }
args = { args = {

View File

@ -352,6 +352,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
language: sample(languages), language: sample(languages),
image: faker.image.unsplash.food(300, 169), image: faker.image.unsplash.food(300, 169),
categoryIds: ['cat16'], categoryIds: ['cat16'],
imageBlurred: true,
imageAspectRatio: 300 / 169, imageAspectRatio: 300 / 169,
}), }),
factory.create('Post', { factory.create('Post', {
@ -398,6 +399,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
author: dewey, author: dewey,
id: 'p10', id: 'p10',
categoryIds: ['cat10'], categoryIds: ['cat10'],
imageBlurred: true,
}), }),
factory.create('Post', { factory.create('Post', {
author: louie, author: louie,
@ -444,6 +446,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
$title: String! $title: String!
$content: String! $content: String!
$categoryIds: [ID] $categoryIds: [ID]
$imageBlurred: Boolean
$imageAspectRatio: Float $imageAspectRatio: Float
) { ) {
CreatePost( CreatePost(
@ -451,6 +454,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
title: $title title: $title
content: $content content: $content
categoryIds: $categoryIds categoryIds: $categoryIds
imageBlurred: $imageBlurred
imageAspectRatio: $imageAspectRatio imageAspectRatio: $imageAspectRatio
) { ) {
id id

File diff suppressed because it is too large Load Diff

View File

@ -44,7 +44,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('.ds-card', davidIrvingPostTitle)
.find('.content-menu-trigger') .find('.content-menu .base-button')
.click({force: true}) .click({force: true})
cy.get('.popover .ds-menu-item-link') cy.get('.popover .ds-menu-item-link')
@ -54,7 +54,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('.ds-card', davidIrvingPostTitle)
.get('.user-content-menu .content-menu-trigger') .get('.user-content-menu .base-button')
.click({ force: true }) .click({ force: true })
cy.get('.popover .ds-menu-item-link') cy.get('.popover .ds-menu-item-link')

View File

@ -1,21 +1,24 @@
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("#nav-search") cy.get(".searchable-input .ds-select-search")
.focus() .focus()
.type(value); .type(value);
}); });
Then("I should have one post in the select dropdown", () => { Then("I should have one item in the select dropdown", () => {
cy.get(".input .ds-select-dropdown").should($li => { cy.get(".searchable-input .ds-select-dropdown").should($li => {
expect($li).to.have.length(1); expect($li).to.have.length(1);
}); });
}); });
Then("the search has no results", () => { Then("the search has no results", () => {
cy.get(".input .ds-select-dropdown").should($li => { cy.get(".searchable-input .ds-select-dropdown").should($li => {
expect($li).to.have.length(1); expect($li).to.have.length(1);
}); });
cy.get(".ds-select-dropdown").should("contain", 'Nothing found'); cy.get(".ds-select-dropdown").should("contain", 'Nothing found');
cy.get(".searchable-input .ds-select-search")
.focus()
.type("{esc}");
}); });
Then("I should see the following posts in the select dropdown:", table => { Then("I should see the following posts in the select dropdown:", table => {
@ -24,26 +27,33 @@ Then("I should see the following posts in the select dropdown:", table => {
}); });
}); });
Then("I should see the following users in the select dropdown:", table => {
cy.get(".ds-heading").should("contain", "Users");
table.hashes().forEach(({ slug }) => {
cy.get(".ds-select-dropdown").should("contain", slug);
});
});
When("I type {string} and press Enter", value => { When("I type {string} and press Enter", value => {
cy.get("#nav-search") cy.get(".searchable-input .ds-select-search")
.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("#nav-search") cy.get(".searchable-input .ds-select-search")
.focus() .focus()
.type(value) .type(value)
.type("{esc}"); .type("{esc}");
}); });
Then("the search field should clear", () => { Then("the search field should clear", () => {
cy.get("#nav-search").should("have.text", ""); cy.get(".searchable-input .ds-select-search").should("have.text", "");
}); });
When("I select an entry", () => { When("I select a post entry", () => {
cy.get(".input .ds-select-dropdown ul li") cy.get(".searchable-input .search-post")
.first() .first()
.trigger("click"); .trigger("click");
}); });
@ -75,3 +85,13 @@ Then(
); );
} }
); );
Then("I select a user entry", () => {
cy.get(".searchable-input .userinfo")
.first()
.trigger("click");
})
Then("I should be on the user's profile", () => {
cy.location("pathname").should("eq", "/profile/user-for-search/search-for-me")
})

View File

@ -119,7 +119,7 @@ Then('they should be able to see my social media links', () => {
}) })
When('I delete a social media link', () => { When('I delete a social media link', () => {
cy.get("a[name='delete']") cy.get(".base-button[title='Delete']")
.click() .click()
}) })
@ -129,7 +129,7 @@ Then('it gets deleted successfully', () => {
}) })
When('I start editing a social media link', () => { When('I start editing a social media link', () => {
cy.get("a[name='edit']") cy.get(".base-button[title='Edit']")
.click() .click()
}) })

View File

@ -243,7 +243,7 @@ When("I type in the following text:", text => {
}); });
Then("I select a category", () => { Then("I select a category", () => {
cy.get("span") cy.get(".base-button")
.contains("Just for Fun") .contains("Just for Fun")
.click(); .click();
}); });
@ -449,7 +449,7 @@ When("I ", name => {
When( When(
"I click on {string} from the content menu in the user info box", "I click on {string} from the content menu in the user info box",
button => { button => {
cy.get(".user-content-menu .content-menu-trigger").click(); cy.get(".user-content-menu .base-button").click();
cy.get(".popover .ds-menu-item-link") cy.get(".popover .ds-menu-item-link")
.contains(button) .contains(button)
.click({ .click({

View File

@ -9,18 +9,23 @@ Feature: Search
| id | title | content | | id | title | content |
| p1 | 101 Essays that will change the way you think | 101 Essays, of course! | | p1 | 101 Essays that will change the way you think | 101 Essays, of course! |
| p2 | No searched for content | will be found in this post, I guarantee | | p2 | No searched for content | will be found in this post, I guarantee |
And we have the following user accounts:
| slug | name | id |
| search-for-me | Search for me | user-for-search |
| not-to-be-found | Not to be found | just-an-id |
Given I am logged in Given I am logged in
Scenario: Search for specific words Scenario: Search for specific words
When I search for "Essays" When I search for "Essays"
Then I should have one post in the select dropdown Then I should have one item in the select dropdown
Then I should see the following posts in the select dropdown: Then I should see the following posts in the select dropdown:
| title | | title |
| 101 Essays that will change the way you think | | 101 Essays that will change the way you think |
Scenario: Press enter starts search Scenario: Press enter starts search
When I type "Essa" and press Enter When I type "Es" and press Enter
Then I should have one post in the select dropdown Then I should have one item in the select dropdown
Then I should see the following posts in the select dropdown: Then I should see the following posts in the select dropdown:
| title | | title |
| 101 Essays that will change the way you think | | 101 Essays that will change the way you think |
@ -31,11 +36,20 @@ Feature: Search
Scenario: Select entry goes to post Scenario: Select entry goes to post
When I search for "Essays" When I search for "Essays"
And I select an entry And I select a post entry
Then I should be on the post's page Then I should be on the post's page
Scenario: Select dropdown content Scenario: Select dropdown content
When I search for "Essays" When I search for "Essays"
Then I should have one post in the select dropdown Then I should have one item in the select dropdown
Then I should see posts with the searched-for term in the select dropdown Then I should see posts with the searched-for term in the select dropdown
And I should not see posts without the searched-for term in the select dropdown And I should not see posts without the searched-for term in the select dropdown
Scenario: Search for users
Given I search for "Search"
Then I should have one item in the select dropdown
And I should see the following users in the select dropdown:
| slug |
| search-for-me |
And I select a user entry
Then I should be on the user's profile

View File

@ -1,814 +0,0 @@
{
"actions": {
"cancel": "Отменить",
"create": "Создать",
"delete": "Удалить",
"edit": "Редактировать",
"loading": "загрузка",
"loadMore": "Загрузить ещё",
"save": "Сохранить"
},
"admin": {
"categories": {
"categoryName": "Имя",
"name": "Категории",
"postCount": "Посты"
},
"dashboard": {
"comments": "Комментарии",
"follows": "Подписки",
"invites": "Приглашения",
"name": "Панель управления",
"notifications": "Уведомления",
"organizations": "Организации",
"posts": "Посты",
"projects": "Проекты",
"shouts": "Выкрики",
"users": "Пользователи"
},
"donations": {
"goal": "Необходимы ежемесячные пожертвования",
"name": "Информация о пожертвованиях",
"progress": "Пожертвования собраны",
"successfulUpdate": "Информация о пожертвованиях успешно обновлена!"
},
"hashtags": {
"name": "Хэштеги",
"nameOfHashtag": "Имя",
"number": "№",
"tagCount": "Посты",
"tagCountUnique": "Пользователи"
},
"invites": {
"description": "Приглашения — это замечательный способ завести друзей в своей сети ...",
"name": "Пригласить пользователей",
"title": "Пригласить людей"
},
"name": "Администрирование",
"notifications": {
"name": "Уведомления"
},
"organizations": {
"name": "Организации"
},
"pages": {
"name": "Страницы"
},
"settings": {
"name": "Настройки"
},
"tags": {
"name": "Теги",
"tagCount": "Посты",
"tagCountUnique": "Пользователи"
},
"users": {
"empty": "Пользователи не найдены",
"form": {
"placeholder": "Электронная почта, имя или описание"
},
"name": "Пользователи",
"table": {
"columns": {
"createdAt": "Дата создания",
"email": "Эл. почта",
"name": "Имя",
"number": "№",
"role": "Роль",
"slug": "Slug"
}
}
}
},
"code-of-conduct": {
"consequences": {
"description": "Если участник сообщества проявляет неприемлемое поведение, ответственные операторы, модераторы и администраторы сети могут принять соответствующие меры, включая, но не ограничиваясь:",
"list": {
"0": "Просьба о немедленном прекращении неприемлемого поведения",
"1": "Блокирование или удаление комментариев",
"2": "Временное исключение из соответствующего поста или другого контента",
"3": "Блокирование или удаление контента",
"4": "Временный запрет на добавление контента",
"5": "Временное исключение из сети",
"6": "Окончательное исключение из сети",
"7": "Передача сведений о нарушениях немецкого законодательства.",
"8": "Пропаганда или поощрение такого поведения."
},
"title": "Последствия неприемлемого поведения"
},
"expected-behaviour": {
"description": "Мы ожидаем и требуем от всех членов сообщества предерживаться следующих правил поведения:",
"list": {
"0": "Будьте внимательны и уважительны к тому, что пишете и делаете.",
"1": "Пытайтесь сотрудничать, прежде чем возникнет конфликт.",
"2": "Воздерживайтесь от поведения и высказываний, унижающих достоинство, дискриминационного или преследующего характера.",
"3": "Будьте внимательны к своему окружению и другим участникам. Информируйте лидеров сообщества об опасных ситуациях, когда кто-либо попал в беду или нарушает настоящий Кодекс поведения, даже если они кажутся незначительными."
},
"title": "Ожидаемое поведение"
},
"get-help": "Если вы стали жертвой или свидетелем неприемлемого поведения или у вас возникли какие-либо другие проблемы, пожалуйста, как можно скорее сообщите об этом организатору сообщества и укажите ссылку на соответствующий контент:",
"preamble": {
"description": "Human Connection - это некоммерческая социальная сеть знаний и действий следующего поколения. Создана людьми для людей. С открытым исходным кодом, справедливая и прозрачная. Для позитивных локальных и глобальных изменений во всех сферах жизни. Мы полностью перестраиваем публичный обмен знаниями, идеями и проектами. Функции Human Connection объединяют людей офлайн и онлайн так что мы можем сделать мир лучше.",
"title": "Преамбула"
},
"purpose": {
"description": "С помощью этих правил поведения мы регулируем основные принципы поведения в нашей социальной сети. При этом Устав ООН по правам человека является нашей ориентацией и лежит в основе нашего понимания ценностей. Правила поведения служат руководящими принципами для личного выступления и общения друг с другом. Любой, кто является активным пользователем в сети Human Connection, публикует сообщения, комментирует или контактирует с другими пользователями, в том числе за пределами сети, признает эти правила поведения обязательными.",
"title": "Цель"
},
"subheader": "социальной сети \"Human Connection gGmbH\"",
"unacceptable-behaviour": {
"description": "В нашем сообществе неприемлемо следующее поведение:",
"list": {
"0": "Дискриминационные посты, комментарии, высказывания или оскорбления, в частности, касающиеся пола, сексуальной ориентации, расы, религии, политической или мировоззренческой ориентации, или инвалидности.",
"1": "Публикация или ссылка на явно порнографические материалы.",
"2": "Прославление или умаление жестоких, или бесчеловечных актов насилия.",
"3": "Публикация персональных данных других лиц без их согласия или угрозы (\"Доксинг\").",
"4": "Преднамеренное запугивание или преследование.",
"5": "Рекламировать продукты и услуги с коммерческим намерением.",
"6": "Преступное поведение или нарушение немецкого права.",
"7": "Одобрение или поощрение недопустимого поведения."
},
"title": "Недопустимое поведение"
}
},
"comment": {
"content": {
"unavailable-placeholder": "...этот комментарий больше не доступен"
},
"delete": "Удалить комментарий",
"edit": "Редактировать комментарий",
"edited": "Изменен",
"menu": {
"delete": "Удалить комментарий",
"edit": "Редактировать комментарий"
},
"show": {
"less": "показать меньше",
"more": "показать больше"
}
},
"common": {
"category": "Категория ::: Категории ::: Категории",
"comment": "Комментарий::: Комментарии::: Комментарии",
"letsTalk": "Давай поговорим",
"loading": "загрузка",
"loadMore": "Загрузить ещё",
"moreInfo": "Больше информации",
"name": "Имя",
"organization": "Организация ::: Организации ::: Организации",
"post": "Пост ::: Посты ::: Посты",
"project": "Проект ::: Проекты ::: Проекты",
"reportContent": "Отчет",
"shout": "Выкрик ::: Выкрики ::: Выкрики",
"tag": "Тег ::: Теги ::: Теги",
"takeAction": "Принять меры",
"user": "Пользователь ::: Пользователи ::: Пользователи",
"validations": {
"categories": "Выберите от одной то трех категорий",
"email": "должен быть корректный адрес электронной почты",
"url": "должен быть корректный URL"
},
"versus": "Против"
},
"components": {
"enter-nonce": {
"form": {
"description": "Откройте папку \\\"Входящие\\\" и введите код из сообщения.",
"next": "Продолжить",
"nonce": "Введите код",
"validations": {
"length": "длина должна быть 6 символов"
}
}
},
"password-reset": {
"change-password": {
"error": "Смена пароля не удалась. Может быть, код безопасности был неправильным?",
"help": "В случае возникновения проблем, не стесняйся обращаться за помощью, отправив нам письмо по адресу:",
"success": "Смена пароля прошла успешно!"
},
"request": {
"form": {
"description": "На указанный адрес электронной почты будет отправлено сообщение с инструкциями для сброса пароля.",
"submit": "Отправить запрос",
"submitted": "На адрес <b>{email}<\/b>было отправлено электронное письмо с дальнейшими инструкциями"
},
"title": "Сбросить пароль"
}
},
"registration": {
"create-user-account": {
"error": "Не удалось создать учетную запись!",
"help": "Может быть, подтверждение было недействительным? В случае возникновения проблем, не стесняйтесь обращаться за помощью, отправив нам письмо по электронной почте:",
"success": "Учетная запись успешно создана!",
"title": "Создать учетную запись"
},
"signup": {
"form": {
"data-privacy": "Я прочитал и понял <a href=\"https:\/\/human-connection.org\/datenschutz\/\" target=\"_blank\"><ds-text bold color=\"primary\" >Заявление о конфиденциальности<\/ds-text><\/a>",
"description": "Для начала работы введите свой адрес электронной почты:",
"errors": {
"email-exists": "Уже есть учетная запись пользователя с этим адресом электронной почты!",
"invalid-invitation-token": "Похоже, что приглашение уже было использовано. Ссылку из приглашения можно использовать только один раз."
},
"invitation-code": "Код приглашения: <b>{code}<\/b>",
"minimum-age": "Мне 18 лет или более",
"no-commercial": "У меня нет коммерческих намерений, и я не представляю коммерческое предприятие или организацию.",
"no-political": "Я не от имени какой-либо партии или политической организации в сети.",
"submit": "Создать учетную запись",
"success": "Письмо со ссылкой для завершения регистрации было отправлено на <b> {email} <\/b>",
"terms-and-condition": "Принимаю <a href=\"\/terms-and-conditions\"><ds-text bold color=\"primary\" >Условия и положения<\/ds-text><\/a>."
},
"title": "Присоединяйся к Human Connection!",
"unavailable": "К сожалению, публичная регистрация пользователей на этом сервере сейчас недоступна."
}
}
},
"contribution": {
"categories": {
"infoSelectedNoOfMaxCategories": "Выбрано {chosen} из {max} категорий"
},
"category": {
"name": {
"animal-protection": "Защита животных",
"art-culture-sport": "Искусство, культура и спорт",
"consumption-sustainability": "Потребление и стабильность",
"cooperation-development": "Сотрудничество и развитие",
"democracy-politics": "Демократия и политика",
"economy-finances": "Экономика и финансы",
"education-sciences": "Образование и наука",
"energy-technology": "Энергия и технологии",
"environment-nature": "Окружающая среда и природа",
"freedom-of-speech": "Свобода слова",
"global-peace-nonviolence": "Глобальный мир и борьба с насилием",
"happiness-values": "Счастье и ценности",
"health-wellbeing": "Здоровье и благополучие",
"human-rights-justice": "Права человека и справедливость",
"it-internet-data-privacy": "ИТ, интернет и конфиденциальность",
"just-for-fun": "Просто для удовольствия"
}
},
"delete": "Удалить",
"edit": "Редактировать",
"emotions-label": {
"angry": "Возмутительно",
"cry": "Плачу",
"funny": "Смешно",
"happy": "Счастлив",
"surprised": "Удивлен"
},
"filterALL": "Просмотреть все посты",
"filterFollow": "Показать сообщения пользователей, на которых я подписан",
"languageSelectLabel": "Язык",
"languageSelectText": "Выберите язык",
"newPost": "Создать пост",
"success": "Сохранено!",
"teaserImage": {
"cropperConfirm": "Подтвердить"
},
"title": "Заголовок"
},
"delete": {
"cancel": "Отменить",
"comment": {
"message": "Вы уверены, что хотите удалить комментарий \"<b>{name}<\/b>\"?",
"success": "Комментарий успешно удален!",
"title": "Удалить комментарий",
"type": "Комментарий"
},
"contribution": {
"message": "Вы уверены, что хотите удалить пост \"<b>{name}<\/b>\"?",
"success": "Пост успешно удален!",
"title": "Удалить пост",
"type": "Пост"
},
"submit": "Удалить"
},
"disable": {
"cancel": "Отменить",
"comment": {
"message": "Вы действительно хотите отключить комментарий от «<b>{name}<\/b>»?",
"title": "Отключить комментарий",
"type": "Комментарий"
},
"contribution": {
"message": "Вы действительно хотите отключить пост «<b>{name}<\/b>»?",
"title": "Отключить пост",
"type": "Пост"
},
"submit": "Отключить",
"success": "Успешно отключен",
"user": {
"message": "Вы действительно хотите отключить пользователя «<b>{name}<\/b>»?",
"title": "Отключить пользователя",
"type": "Пользователь"
}
},
"donations": {
"amount-of-total": "{amount} из {total} € собрано",
"donate-now": "Пожертвуйте сейчас",
"donations-for": "Пожертвования для"
},
"editor": {
"embed": {
"always_allow": "Всегда отображать содержимое сторонних производителей (эту настройку можно изменить в любое время).",
"data_privacy_info": "Ваши данные еще не были переданы третьим лицам. Если вы воспроизведёте это видео, следующий провайдер, вероятно, зарегистрирует ваши данные пользователя:",
"data_privacy_warning": "Предупреждение о конфиденциальности данных!",
"play_now": "Смотреть сейчас"
},
"hashtag": {
"addHashtag": "Новый хэштег",
"addLetter": "Введите букву",
"noHashtagsFound": "Хэштеги не найдены"
},
"mention": {
"noUsersFound": "Пользователи не найдены"
},
"placeholder": "Поделитесь своими вдохновляющими мыслями ..."
},
"filter-menu": {
"clearSearch": "Очистить поиск",
"hashtag-search": "Поиск по #{hashtag}",
"title": "Ваш фильтр пузыря"
},
"filter-posts": {
"categories": {
"all": "Все",
"header": "Категории"
},
"followers": {
"label": "Мои подписки"
},
"general": {
"header": "Другие фильтры"
},
"language": {
"all": "Все",
"header": "Языки"
}
},
"followButton": {
"follow": "Подписаться",
"following": "Вы подписаны"
},
"index": {
"change-filter-settings": "Измените настройки фильтра, чтобы получить больше результатов.",
"no-results": "Посты не найдены."
},
"login": {
"copy": "Авторизуйтесь, если у вас уже есть учетная запись Human Connection.",
"email": "Электронная почта",
"failure": "Неверный адрес электронной почты или пароль.",
"forgotPassword": "Забыли пароль?",
"hello": "Здравствуйте",
"login": "Вход",
"logout": "Выйти",
"moreInfo": "Что такое Human Connection?",
"moreInfoHint": "на страницу проекта",
"moreInfoURL": "https:\/\/human-connection.org\/en\/",
"no-account": "У вас нет аккаунта?",
"password": "Пароль",
"register": "Зарегистрируйтесь",
"success": "Вы вошли в систему!"
},
"maintenance": {
"explanation": "В данный момент мы проводим плановое техническое обслуживание, пожалуйста, повторите попытку позже.",
"questions": "Любые вопросы или сообщения о проблемах отправляйте на электронную почту",
"title": "Human Connection на техническом обслуживании"
},
"moderation": {
"name": "Модерация",
"reports": {
"author": "Автор",
"content": "Содержа́ние",
"decideButton": "Подтвердить",
"decided": "Решил",
"decideModal": {
"cancel": "Отменить",
"Comment": {
"disable": {
"message": "Вы действительно хотите, чтобы комментарий \"<b>{name}<\/b>\" остановиться и <b>отключен<\/b>?",
"title": "Окончательно отключить комментарий"
},
"enable": {
"message": "Вы действительно хотите, чтобы комментарий \"<b>{name}<\/b>\" остановиться и <b>включен<\/b>?",
"title": "Окончательно включить комментарий"
}
},
"Post": {
"disable": {
"message": "Вы действительно хотите, чтобы пост \"<b>{name}<\/b>\" остановиться и <b>отключен<\/b>?",
"title": "Окончательно отключить пост"
},
"enable": {
"message": "Вы действительно хотите, чтобы пост \"<b>{name}<\/b>\" остановиться и <b>включен<\/b>?",
"title": "Окончательно включить пост"
}
},
"submit": "Подтвердить решение",
"User": {
"disable": {
"message": "Вы действительно хотите, чтобы пользователь \"<b>{name}<\/b>\" остановиться и <b>отключен<\/b>?",
"title": "Окончательно отключить пользователя"
},
"enable": {
"message": "Вы уверены, что хотите поделиться пользователем \"<b>{name}<\/b>\"?",
"title": "Окончательно включить пост"
}
}
},
"decision": "Решение",
"DecisionSuccess": "Решил успешно!",
"disabled": "Отключен",
"disabledAt": "Отключено на",
"disabledBy": "Отключил(а)",
"empty": "Поздравляю, модерировать нечего.",
"enabled": "Включен",
"enabledAt": "Включено на",
"enabledBy": "Включено с",
"filterLabel": {
"all": "Все",
"closed": "Закрыто",
"reviewed": "Рассмотренный",
"unreviewed": "Нерассмотренный"
},
"moreDetails": "Посмотреть подробности",
"name": "Отчеты",
"noDecision": "Нет решения!",
"numberOfUsers": "{count} пользователи",
"previousDecision": "Предыдущее решение:",
"reasonCategory": "Категория",
"reasonDescription": "Описание",
"reportedOn": "Дата",
"reporter": "Сообщил(а)",
"status": "Текущее состояние",
"submitter": "Сообщил(а)"
}
},
"notifications": {
"comment": "Комментарий",
"content": "Контент",
"empty": "Извините, на данный момент у вас нет уведомлений.",
"filterLabel": {
"all": "Все",
"read": "Прочитанные",
"unread": "Непрочитанные"
},
"pageLink": "Все уведомления",
"post": "Пост",
"reason": {
"commented_on_post": "Комментарий к посту...",
"mentioned_in_comment": "Упоминание в комментарии....",
"mentioned_in_post": "Упоминание в посте...."
},
"title": "Уведомления",
"user": "Пользователь"
},
"post": {
"comment": {
"submit": "Комментировать",
"submitted": "Комментарий отправлен",
"updated": "Изменения сохраненные"
},
"edited": "Изменен",
"menu": {
"delete": "Удалить пост",
"edit": "Редактировать пост",
"pin": "Закрепить пост",
"pinnedSuccessfully": "Пост больше не закреплен!",
"unpin": "Открепить пост",
"unpinnedSuccessfully": "Пост успешно не закреплено!"
},
"moreInfo": {
"description": "Здесь содержится дополнительная информация по теме.",
"name": "Дополнительная информация",
"title": "Дополнительная информация",
"titleOfCategoriesSection": "Категории",
"titleOfHashtagsSection": "Хэштеги",
"titleOfRelatedContributionsSection": "Похожие посты"
},
"name": "Пост",
"pinned": "Объявление",
"takeAction": {
"name": "Действовать"
}
},
"profile": {
"commented": "Прокомментированные",
"follow": "Подписаться",
"followers": "Подписчики",
"following": "Подписки",
"invites": {
"description": "Введите адрес электронной почты для приглашения.",
"emailPlaceholder": "Электронная почта для приглашения",
"title": "Пригласите кого-нибудь в Human Connection!"
},
"memberSince": "Участник с",
"name": "Мой профиль",
"network": {
"andMore": "и ещё {number} человек... ::: и ещё {number} человека... ::: и ещё {number} человек...",
"followedBy": "ваши подписчики:",
"followedByNobody": "у вас нет подписчиков.",
"following": "подписан на:",
"followingNobody": "ни на кого не подписан.",
"title": "Сеть"
},
"shouted": "С выкриками",
"socialMedia": "Где еще я могу найти",
"userAnonym": "Анонимный"
},
"quotes": {
"african": {
"author": "Африканская пословица",
"quote": "Много маленьких людей делают много маленьких вещей во многих маленьких местах, что может изменить мир до неузнаваемости."
}
},
"release": {
"cancel": "Отменить",
"comment": {
"error": "Вы уже сообщили о комментарии!",
"message": "Вы уверены, что хотите показать комментарий \"<b>{name}<\/b>\"?",
"title": "Показать комментарий",
"type": "Комментарий"
},
"contribution": {
"error": "Вы уже сообщили о посте!",
"message": "Вы уверены, что хотите показать пост \"<b>{name}<\/b>\"?",
"title": "Показать пост",
"type": "Пост"
},
"submit": "Показать",
"success": "Успешно показан!",
"user": {
"error": "Вы уже сообщили о пользователе!",
"message": "Вы уверены, что хотите показать пользователя \"<b>{name}<\/b>\"?",
"title": "Показать пользователя",
"type": "Пользователь"
}
},
"report": {
"cancel": "Отменить",
"comment": {
"error": "Вы уже сообщили о посте!",
"message": "Вы действительно хотите сообщить о посте \"<b> {name} <\/b>\"?",
"title": "Пожаловаться на комментарий",
"type": "Комментарий"
},
"contribution": {
"error": "Вы уже сообщили о посте!",
"message": "Вы действительно хотите сообщить о посте \"<b>{name}<\/b>\"?",
"title": "Пожаловаться на пост",
"type": "Пожаловаться на пост"
},
"reason": {
"category": {
"invalid": "Пожалуйста, выберите подходящую категорию",
"label": "Выберите категорию:",
"options": {
"advert_products_services_commercial": "Реклама продуктов и услуг с коммерческим намерением.",
"criminal_behavior_violation_german_law": "Уголовное поведение или нарушении немецкого права.",
"discrimination_etc": "Дискриминационные посты, комментарии, заявления или оскорбления.",
"doxing": "Публикация персональных данных других лиц без их согласия или угроза публикации (\"Доксинг\").",
"glorific_trivia_of_cruel_inhuman_acts": "Прославление или умаление жестоких, или бесчеловечных актов насилия.",
"intentional_intimidation_stalking_persecution": "Преднамеренное запугивание или преследование.",
"other": "Другое ...",
"pornographic_content_links": "Публикация или ссылка на явно порнографический материал."
},
"placeholder": "Категория ..."
},
"description": {
"label": "Пожалуйста, объясните, почему хотите об этом сообщить?",
"placeholder": "Дополнительная информация ..."
}
},
"submit": "Отправить",
"success": "Спасибо за сообщение!",
"user": {
"error": "Вы уже сообщили о пользователе!",
"message": "Вы действительно хотите сообщить о пользователе \"<b>{name}<\/b>\"?",
"title": "Пожаловаться на пользователя",
"type": "Пользователь"
}
},
"search": {
"failed": "Ничего не найдено",
"hint": "Что вы хотите найти?",
"placeholder": "Поиск"
},
"settings": {
"blocked-users": {
"block": "Блокировать",
"columns": {
"name": "Имя",
"slug": "Псевдоним",
"unblock": "Разблокировать"
},
"empty": "Вы пока никого не блокировали.",
"explanation": {
"closing": "На данный момент этого должно быть достаточно, чтобы заблокированные пользователи больше вас не беспокоили.",
"intro": "Если блокируете другого пользователя, происходит следующее:",
"notifications": "Заблокированные пользователи больше не будут получать уведомления об упоминаниях в ваших постах.",
"search": "Посты заблокированных пользователей не отображаются в результатах поиска.",
"their-perspective": "И наоборот — заблокированный пользователь больше не видит ваши посты в своей ленте.",
"your-perspective": "Посты заблокированного пользователя не отображаются в персональной ленте."
},
"how-to": "Вы можете блокировать других пользователей на странице их профиля с помощью меню профиля.",
"name": "Заблокированные пользователи",
"unblock": "Разблокировать пользователей",
"unblocked": "{name} - снова разблокирован"
},
"data": {
"labelBio": "О себе",
"labelCity": "Город или регион",
"labelName": "Имя",
"labelSlug": "Уникальное имя пользователя",
"name": "Персональные данные",
"namePlaceholder": "Маша Медведева",
"success": "Персональные данные были успешно обновлены!"
},
"delete": {
"name": "Удалить аккаунт"
},
"deleteUserAccount": {
"accountDescription": "Обратите внимание, что ваши посты и комментарии важны для сообщества. Если вы все равно хотите их удалить, то вы должны отметить соответствующие опции ниже.",
"accountWarning": "Вы <b>НЕ СМОЖЕТЕ<\/b> восстановить свой аккаунт, посты или комментарии после удаления.",
"commentedCount": "Удалить мои комментарии: {count}",
"contributionsCount": "Удалить мои посты: {count}",
"name": "Удалить данные",
"pleaseConfirm": "<b class='is-danger'>Разрушительное действие!<\/b> Введите <b>{confirm}<\/b> для подтверждения.",
"success": "Аккаунт успешно удален!"
},
"download": {
"name": "Скачать данные"
},
"email": {
"change-successful": "Адрес электронной почты был успешно изменен.",
"labelEmail": "Адрес электронной почты",
"labelNewEmail": "Новый адрес электронной почты",
"labelNonce": "Введите свой код",
"name": "Электронная почта",
"submitted": "Электронное письмо с подтверждением отправлено на <b>{email}<\/b>.",
"success": "Новый адрес электронной почты был зарегистрирован.",
"validation": {
"same-email": "Это текущий адрес электронной почты."
},
"verification-error": {
"explanation": "Причины могут быть разными:",
"message": "Адрес электронной почты не может быть изменен.",
"reason": {
"invalid-nonce": "Правильно ли указан код подтверждения?",
"no-email-request": "Вы уверены, что отправляли запрос на изменение своего адреса электронной почты?"
},
"support": "Если проблема сохраняется, пожалуйста, свяжитесь с нами по электронной почте"
}
},
"embeds": {
"info-description": "Вот список сторонних провайдеров, чей контент может отображаться в форме вставок кода, например, в виде встроенных видео:",
"name": "Сторонний контент",
"status": {
"change": {
"allow": "Конечно.",
"deny": "Нет, не надо",
"question": "Вы хотите, чтобы вставки кода сторонних провайдеров всегда отображались?"
},
"description": "Значение по умолчанию -",
"disabled": {
"off": "сначала не отображать вставки кода сторонних провайдеров",
"on": "сразу отображать вставки кода сторонних провайдеров"
}
}
},
"invites": {
"name": "Приглашения"
},
"languages": {
"name": "Языки"
},
"name": "Настройки",
"organizations": {
"name": "Мои организации"
},
"privacy": {
"make-shouts-public": "Публиковать в моем публичном профиле статьи в которых я участвовал",
"name": "Конфиденциальность",
"success-update": "Настройки приватности сохранены"
},
"security": {
"change-password": {
"button": "Изменить пароль",
"label-new-password": "Новый пароль",
"label-new-password-confirm": "Подтверждение пароля",
"label-old-password": "Старый пароль",
"message-new-password-confirm-required": "Требуется подтверждение пароля",
"message-new-password-missmatch": "Пароли не совпадают",
"message-new-password-required": "Требуется новый пароль",
"message-old-password-required": "Требуется свой старый пароль",
"passwordSecurity": "Безопасность пароля",
"passwordStrength0": "Очень небезопасный",
"passwordStrength1": "Небезопасный",
"passwordStrength2": "Посредственный",
"passwordStrength3": "Надежный",
"passwordStrength4": "Очень надежный",
"success": "Пароль успешно изменен!"
},
"name": "Безопасность"
},
"social-media": {
"name": "Социальные Медиа",
"placeholder": "Ссылка на профиль социальной сети",
"requireUnique": "Ссылка уже существует",
"submit": "Добавить ссылку",
"successAdd": "Добавлены социальные меди. Профиль обновлен!",
"successDelete": "Социальные Меди удалены. Профиль обновлен!"
},
"validation": {
"slug": {
"alreadyTaken": "Это имя пользователя уже занято.",
"regex": "Допускаются только строчные буквы, цифры, подчеркивания или дефисы."
}
}
},
"shoutButton": {
"shouted": "выкрикнули"
},
"site": {
"back-to-login": "Вернуться на страницу входа",
"bank": "банковский счет",
"changelog": "Изменения",
"code-of-conduct": "Кодекс поведения",
"contact": "Контакт",
"data-privacy": "Конфиденциальность",
"director": "Управляющий директор",
"error-occurred": "Произошла ошибка.",
"faq": "ЧаВо (FAQ)",
"germany": "Германия",
"imprint": "Импрессум",
"made": "Сделано с &#10084;",
"register": "Регистрационный номер",
"responsible": "ответственный за содержание этой страницы (§ 55 Abs. 2 RStV)",
"taxident": "UST-ID. в соответствии с §27a Закона о налоге с продаж Германии:",
"termsAndConditions": "Условия и положения",
"thanks": "Спасибо!",
"tribunal": "Суд регистрации"
},
"store": {
"posts": {
"orderBy": {
"newest": {
"label": "Сначала новые"
},
"oldest": {
"label": "Сначала старые"
}
}
}
},
"termsAndConditions": {
"addition": {
"description": "<a href=\"https:\/\/human-connection.org\/events\/\" target=\"_blank\" > https:\/\/human-connection.org\/events\/ <\/a>",
"title": "Кроме того, мы регулярно проводим мероприятия, где вы также можете\\nподелиться своими впечатлениями и задать вопросы. Информацию о текущих событиях можно найти здесь:"
},
"agree": "Я согласен(на)!",
"code-of-conduct": {
"description": "Наш кодекс поведения служит руководством для личного поведения и взаимодействия друг с другом. Каждый пользователь социальной сети Human Connection, который пишет статьи, комментирует или вступает в контакт с другими пользователями, даже за пределами сети, признает эти правила поведения обязательными. <a href=\"https:\/\/alpha.human-connection.org\/code-of-conduct\" target=\"_blank\"> https:\/\/alpha.human-connection.org\/code-of-conduct<\/a>",
"title": "Кодекс поведения"
},
"errors-and-feedback": {
"description": "Мы прилагаем все усилия для обеспечения безопасности и доступности нашей сети и данных. Каждый новый выпуск программного обеспечения проходит как автоматическое, так и ручное тестирование. Однако могут возникнуть непредвиденные ошибки. Поэтому мы благодарны за любые обнаруженные ошибки. Вы можете сообщить о любых обнаруженных ошибках, отправив электронное письмо в службу поддержки по адресу support@human-connection.org",
"title": "Ошибки и обратная связь"
},
"help-and-questions": {
"description": "Для справки и вопросов мы собрали для вас исчерпывающую подборку часто задаваемых вопросов и ответов (FAQ). Вы можете найти их здесь: <a href=\"https:\/\/support.human-connection.org\/kb\/\" target=\"_blank\" > https:\/\/support.human-connection.org\/kb\/ <\/a>",
"title": "Помощь и вопросы"
},
"moderation": {
"description": "Пока наши финансовые возможности не позволяют нам реализовать полноценную систему модерации, поэтому мы осуществляем упрощенную модерацию собственными силами и с помощью волонтёров. Мы специально обучаем этих модераторов, поэтому только они принимают соответствующие решения. Модераторы действуют анонимно. Вы можете сообщать нам о постах, комментариях и пользователях (например, если они предоставляют информацию в своем профиле или имеют изображения, которые нарушают настоящие Условия использования). При обращении вы можете указать причину и дать краткое пояснение. Мы рассмотрим обращение и применим санкции в случае необходимости, например, путем блокировки постов, комментариев или пользователей. К сожалению, в настоящее время ни вы ни пострадавший пользователь не получите от нас обратной связи, но мы планируем ряд улучшений в этом направлении. Несмотря на это, мы оставляем за собой право на применение санкций по причинам, которые не могут быть или ещё не указаны в нашем Кодексе поведения или настоящих Условиях использования.",
"title": "Модерация"
},
"newTermsAndConditions": "Новые условия и положения",
"no-commercial-use": {
"description": "Использование Human Connection сети не допускается в коммерческих целях. Это включает, но не ограничивается рекламой продуктов с коммерческими целями, размещением партнерских ссылок, прямым привлечением пожертвований или предоставлением финансовой поддержки для целей, которые не признаются благотворительными для целей налогообложения.",
"title": "Нет коммерческого использования"
},
"privacy-statement": {
"description": "Наша сеть — это социальная сеть знаний и действий. Поэтому для нас особенно важно, чтобы как можно больше контента было общедоступным. В процессе развития нашей сети будет добавлено больше возможностей для управления видимостью личных данных. Об этих новых функциях мы сообщим дополнительно. В противном случае вы должны думать о том, какие личные данные вы раскрываете о себе (или других). Это особенно актуально для содержания постов и комментариев, поскольку они имеют в основном общедоступный характер. Позже появятся возможности ограничения видимости вашего профиля. Часть условий использования — это наша политика конфиденциальности, которая информирует вас об обработке персональных данных в нашей сети: <a href=\"https:\/\/human-connection.org\/datenschutz\/#netzwerk\" target=\"_blank\">https:\/\/human-connection.org\/datenschutz\/#netzwerk<\/a> или <a href=\"https:\/\/human-connection.org\/datenschutz\/\" target=\"_blank\">https:\/\/human-connection.org\/datenschutz<\/a>. Наше заявление о конфиденциальности корректируется в соответствии с законодательством и характеристиками нашей сети и является действительной в настоящей версии.",
"title": "Заявление о конфиденциальности"
},
"terms-of-service": {
"description": "Следующие условия использования являются основой для использования нашей сети. При регистрации вы должны принять их, а мы при необходимости сообщим вам об изменениях. Сеть Human Connection работает в Германии и поэтому регулируется немецким законодательством. Место юрисдикции - Kirchheim \/ Teck. Подробности в выходных данных: <a href=\"https:\/\/human-connection.org\/en\/imprint\" target=\"_blank\" >https:\/\/human-connection.org\/en\/imprint<\/a>.",
"title": "Условия обслуживания"
},
"termsAndConditionsConfirmed": "Я прочитал(а) и подтверждаю <a href=\"\/terms-and-conditions\" target=\"_blank\">Условия и положения<\/a>.",
"termsAndConditionsNewConfirm": "Я прочитал(а) и согласен(на) с новыми условиями.",
"termsAndConditionsNewConfirmText": "Пожалуйста, ознакомьтесь с новыми условиями использования!",
"use-and-license": {
"description": "Если размещаемый в сети контент защищен правами на интеллектуальную собственность, вы предоставляете нам неисключительную, передаваемую, сублицензируемую и всемирную лицензию на использование этого контента для публикации в нашей сети. Эта лицензия заканчивается, как только вы удаляете свой контент или учетную запись. Помните, что другие пользователи могут продолжать делиться вашим контентом, и мы не можем его удалить.",
"title": "Использование и лицензия"
}
},
"user": {
"avatar": {
"submitted": "Успешная загрузка!"
}
}
}

View File

@ -1,4 +1,4 @@
FROM neo4j:3.5.13-enterprise FROM neo4j:3.5.14-enterprise
LABEL Description="Neo4J database of the Social Network Human-Connection.org with preinstalled database constraints and indices" Vendor="Human Connection gGmbH" Version="0.0.1" Maintainer="Human Connection gGmbH (developer@human-connection.org)" LABEL Description="Neo4J database of the Social Network Human-Connection.org with preinstalled database constraints and indices" Vendor="Human Connection gGmbH" Version="0.0.1" Maintainer="Human Connection gGmbH (developer@human-connection.org)"
ARG BUILD_COMMIT ARG BUILD_COMMIT

View File

@ -2,7 +2,6 @@
ENV_FILE=$(dirname "$0")/.env ENV_FILE=$(dirname "$0")/.env
[[ -f "$ENV_FILE" ]] && source "$ENV_FILE" [[ -f "$ENV_FILE" ]] && source "$ENV_FILE"
if [ -z "$NEO4J_USERNAME" ] || [ -z "$NEO4J_PASSWORD" ]; then if [ -z "$NEO4J_USERNAME" ] || [ -z "$NEO4J_PASSWORD" ]; then
echo "Please set NEO4J_USERNAME and NEO4J_PASSWORD environment variables." echo "Please set NEO4J_USERNAME and NEO4J_PASSWORD environment variables."
echo "Setting up database constraints and indexes will probably fail because of authentication errors." echo "Setting up database constraints and indexes will probably fail because of authentication errors."
@ -21,7 +20,8 @@ CALL db.indexes();
' | cypher-shell ' | cypher-shell
echo ' echo '
CALL db.index.fulltext.createNodeIndex("full_text_search",["Post"],["title", "content"]); CALL db.index.fulltext.createNodeIndex("post_fulltext_search",["Post"],["title", "content"]);
CALL db.index.fulltext.createNodeIndex("user_fulltext_search",["User"],["name", "slug"]);
CREATE CONSTRAINT ON (p:Post) ASSERT p.id IS UNIQUE; CREATE CONSTRAINT ON (p:Post) ASSERT p.id IS UNIQUE;
CREATE CONSTRAINT ON (c:Comment) ASSERT c.id IS UNIQUE; CREATE CONSTRAINT ON (c:Comment) ASSERT c.id IS UNIQUE;
CREATE CONSTRAINT ON (c:Category) ASSERT c.id IS UNIQUE; CREATE CONSTRAINT ON (c:Category) ASSERT c.id IS UNIQUE;

View File

@ -1,6 +1,6 @@
{ {
"name": "human-connection", "name": "human-connection",
"version": "0.1.13", "version": "0.2.1",
"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",
@ -21,27 +21,27 @@
"version": "auto-changelog -p" "version": "auto-changelog -p"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.7.5", "@babel/core": "^7.8.3",
"@babel/preset-env": "^7.7.6", "@babel/preset-env": "^7.8.3",
"@babel/register": "^7.7.4", "@babel/register": "^7.8.3",
"auto-changelog": "^1.16.2", "auto-changelog": "^1.16.2",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"codecov": "^3.6.1", "codecov": "^3.6.1",
"cross-env": "^6.0.3", "cross-env": "^6.0.3",
"cucumber": "^6.0.5", "cucumber": "^6.0.5",
"cypress": "^3.8.0", "cypress": "^3.8.2",
"cypress-cucumber-preprocessor": "^1.19.0", "cypress-cucumber-preprocessor": "^2.0.1",
"cypress-file-upload": "^3.5.1", "cypress-file-upload": "^3.5.3",
"cypress-plugin-retries": "^1.5.0", "cypress-plugin-retries": "^1.5.2",
"date-fns": "^2.8.1", "date-fns": "^2.9.0",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"expect": "^24.9.0", "expect": "^24.9.0",
"faker": "Marak/faker.js#master", "faker": "Marak/faker.js#master",
"graphql-request": "^1.8.2", "graphql-request": "^1.8.2",
"neo4j-driver": "^1.7.6", "neo4j-driver": "^4.0.1",
"neode": "^0.3.6", "neode": "^0.3.7",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"slug": "^1.1.0" "slug": "^2.1.0"
}, },
"resolutions": { "resolutions": {
"set-value": "^2.0.1" "set-value": "^2.0.1"

View File

@ -1,4 +1,4 @@
FROM node:13.3.0-alpine as base FROM node:lts-alpine as base
LABEL Description="Web Frontend of the Social Network Human-Connection.org" Vendor="Human-Connection gGmbH" Version="0.0.1" Maintainer="Human-Connection gGmbH (developer@human-connection.org)" LABEL Description="Web Frontend of the Social Network Human-Connection.org" Vendor="Human-Connection gGmbH" Version="0.0.1" Maintainer="Human-Connection gGmbH (developer@human-connection.org)"
EXPOSE 3000 EXPOSE 3000

View File

@ -1,4 +1,4 @@
FROM node:13.3.0-alpine as build FROM node:lts-alpine as build
LABEL Description="Maintenance page of the Social Network Human-Connection.org" Vendor="Human-Connection gGmbH" Version="0.0.1" Maintainer="Human-Connection gGmbH (developer@human-connection.org)" LABEL Description="Maintenance page of the Social Network Human-Connection.org" Vendor="Human-Connection gGmbH" Version="0.0.1" Maintainer="Human-Connection gGmbH (developer@human-connection.org)"
EXPOSE 3000 EXPOSE 3000

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>level-down</title>
<path d="M5 5h17v19.063l4.281-4.281 1.438 1.438-6 6-0.719 0.688-0.719-0.688-6-6 1.438-1.438 4.281 4.281v-17.063h-15v-2z"></path>
</svg>

After

Width:  |  Height:  |  Size: 293 B

View File

@ -0,0 +1,65 @@
@mixin buttonStates($color-scheme: primary, $filled: false) {
$main-color: $color-primary;
$active-color: $color-primary-dark;
$hover-color: $color-primary-light;
@if $color-scheme == danger {
$main-color: $color-danger;
$active-color: $color-danger-dark;
$hover-color: $color-danger-light;
}
color: $main-color;
border-color: $main-color;
background-color: transparent;
transition: background-color $duration-short;
&:focus {
outline: $border-size-base dashed $main-color;
}
&:enabled {
&:hover {
color: $color-neutral-100;
border-color: $main-color;
background-color: $main-color;
}
&:active {
color: $color-neutral-100;
border-color: $active-color;
background-color: $active-color;
}
}
&:disabled {
color: $color-neutral-60;
border-color: $color-neutral-60;
cursor: default;
}
@if $filled {
color: $color-neutral-100;
border-color: $main-color;
background-color: $main-color;
&:enabled {
&:hover {
border-color: $hover-color;
background-color: $hover-color;
}
&:active {
color: $color-neutral-100;
border-color: $active-color;
background-color: $active-color;
}
}
&:disabled {
color: $color-neutral-100;
background-color: $color-neutral-60;
border-color: $color-neutral-60;
}
}
}

View File

@ -0,0 +1,11 @@
* {
box-sizing: border-box;
}
button {
padding: 0;
background: transparent;
border: none;
font-family: inherit;
font-size: inherit;
}

View File

@ -4,6 +4,8 @@
*/ */
$color-primary: rgb(23, 181, 63); $color-primary: rgb(23, 181, 63);
$color-primary-light: rgb(96, 214, 98);
$color-primary-dark: rgb(25, 122, 49);
$color-primary-active: rgb(25, 194, 67); $color-primary-active: rgb(25, 194, 67);
$color-primary-inverse: rgb(241, 253, 244); $color-primary-inverse: rgb(241, 253, 244);
$color-secondary: rgb(0, 142, 230); $color-secondary: rgb(0, 142, 230);
@ -13,6 +15,8 @@
$color-success-active: rgb(26, 203, 71); $color-success-active: rgb(26, 203, 71);
$color-success-inverse: rgb(241, 253, 244); $color-success-inverse: rgb(241, 253, 244);
$color-danger: rgb(219, 57, 36); $color-danger: rgb(219, 57, 36);
$color-danger-light: rgb(242, 97, 65);
$color-danger-dark: rgb(158, 43, 28);
$color-danger-active: rgb(224, 81, 62); $color-danger-active: rgb(224, 81, 62);
$color-danger-inverse: rgb(253, 243, 242); $color-danger-inverse: rgb(253, 243, 242);
$color-warning: rgb(230, 121, 25); $color-warning: rgb(230, 121, 25);
@ -246,6 +250,21 @@ $size-avatar-base: 44px;
$size-avatar-large: 64px; $size-avatar-large: 64px;
$size-avatar-x-large: 114px; $size-avatar-x-large: 114px;
/**
* @tokens Size Buttons
* @presenter Spacing
*/
$size-button-base: 36px;
$size-button-small: 26px;
/**
* @tokens Size Buttons
* @presenter Spacing
*/
$size-icon-base: 16px;
/** /**
* @tokens Shadow * @tokens Shadow
* @presenter Shadow * @presenter Shadow
@ -291,6 +310,7 @@ $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;
/** /**
* @tokens Media Query * @tokens Media Query

View File

@ -3,16 +3,16 @@
<ds-flex :gutter="{ base: 'xx-small', md: 'small', lg: 'xx-small' }"> <ds-flex :gutter="{ base: 'xx-small', md: 'small', lg: 'xx-small' }">
<div v-for="category in categories" :key="category.id"> <div v-for="category in categories" :key="category.id">
<ds-flex-item> <ds-flex-item>
<ds-button <base-button
size="small"
:data-test="categoryButtonsId(category.id)" :data-test="categoryButtonsId(category.id)"
@click.prevent="toggleCategory(category.id)" @click="toggleCategory(category.id)"
:primary="isActive(category.id)" :filled="isActive(category.id)"
:disabled="isDisabled(category.id)" :disabled="isDisabled(category.id)"
:icon="category.icon"
size="small"
> >
<base-icon :name="category.icon" />
{{ $t(`contribution.category.name.${category.slug}`) }} {{ $t(`contribution.category.name.${category.slug}`) }}
</ds-button> </base-button>
</ds-flex-item> </ds-flex-item>
</div> </div>
</ds-flex> </ds-flex>

View File

@ -19,6 +19,9 @@ export default {
.category-tag { .category-tag {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
&.language {
float: right;
}
> .base-icon { > .base-icon {
margin-right: $space-xx-small; margin-right: $space-xx-small;

View File

@ -54,14 +54,15 @@
</button> </button>
</div> </div>
<ds-space margin-bottom="small" /> <ds-space margin-bottom="small" />
<ds-button <base-button
:title="this.$t('post.comment.reply')" :title="this.$t('post.comment.reply')"
icon="level-down" icon="level-down"
@click.prevent="reply" @click.prevent="reply"
v-scroll-to="'.editor'" v-scroll-to="'.editor'"
circle
class="reply-button" class="reply-button"
size="small" size="small"
></ds-button> ></base-button>
</ds-card> </ds-card>
</div> </div>
</template> </template>
@ -75,6 +76,7 @@ import ContentViewer from '~/components/Editor/ContentViewer'
import HcCommentForm from '~/components/CommentForm/CommentForm' import HcCommentForm from '~/components/CommentForm/CommentForm'
import CommentMutations from '~/graphql/CommentMutations' import CommentMutations from '~/graphql/CommentMutations'
import scrollToAnchor from '~/mixins/scrollToAnchor.js' import scrollToAnchor from '~/mixins/scrollToAnchor.js'
import BaseButton from '~/components/_new/generic/BaseButton/BaseButton'
export default { export default {
mixins: [scrollToAnchor], mixins: [scrollToAnchor],
@ -94,6 +96,7 @@ export default {
ContentMenu, ContentMenu,
ContentViewer, ContentViewer,
HcCommentForm, HcCommentForm,
BaseButton,
}, },
props: { props: {
routeHash: { type: String, default: () => '' }, routeHash: { type: String, default: () => '' },

View File

@ -74,7 +74,7 @@ describe('CommentForm.vue', () => {
it('calls `clear` method when the cancel button is clicked', async () => { it('calls `clear` method when the cancel button is clicked', async () => {
wrapper.vm.updateEditorContent('ok') wrapper.vm.updateEditorContent('ok')
await wrapper.find('.cancelBtn').trigger('submit') await wrapper.find('[data-test="cancel-button"]').trigger('submit')
expect(cancelMethodSpy).toHaveBeenCalledTimes(1) expect(cancelMethodSpy).toHaveBeenCalledTimes(1)
}) })
@ -162,13 +162,13 @@ describe('CommentForm.vue', () => {
describe('cancel button is clicked', () => { describe('cancel button is clicked', () => {
it('calls `closeEditWindow` method', async () => { it('calls `closeEditWindow` method', async () => {
wrapper.vm.updateEditorContent('ok') wrapper.vm.updateEditorContent('ok')
await wrapper.find('.cancelBtn').trigger('submit') await wrapper.find('[data-test="cancel-button"]').trigger('submit')
expect(closeMethodSpy).toHaveBeenCalledTimes(1) expect(closeMethodSpy).toHaveBeenCalledTimes(1)
}) })
it('emits `showEditCommentMenu` event', async () => { it('emits `showEditCommentMenu` event', async () => {
wrapper.vm.updateEditorContent('ok') wrapper.vm.updateEditorContent('ok')
await wrapper.find('.cancelBtn').trigger('submit') await wrapper.find('[data-test="cancel-button"]').trigger('submit')
expect(wrapper.emitted('showEditCommentMenu')).toEqual([[false]]) expect(wrapper.emitted('showEditCommentMenu')).toEqual([[false]])
}) })
}) })

View File

@ -1,28 +1,22 @@
<template> <template>
<ds-form v-model="form" @submit="handleSubmit"> <ds-form v-model="form" @submit="handleSubmit" class="comment-form">
<template slot-scope="{ errors }"> <template slot-scope="{ errors }">
<ds-card> <ds-card>
<!-- with client-only the content is not shown --> <!-- 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" />
<ds-space /> <div class="buttons">
<ds-flex :gutter="{ base: 'small', md: 'small', sm: 'x-large', xs: 'x-large' }"> <base-button
<ds-flex-item :width="{ base: '0%', md: '50%', sm: '0%', xs: '0%' }" /> :disabled="disabled && !update"
<ds-flex-item :width="{ base: '40%', md: '20%', sm: '30%', xs: '30%' }"> @click="handleCancel"
<ds-button data-test="cancel-button"
:disabled="disabled && !update" danger
ghost >
class="cancelBtn" {{ $t('actions.cancel') }}
@click.prevent="handleCancel" </base-button>
> <base-button type="submit" :loading="loading" :disabled="disabled || errors" filled>
{{ $t('actions.cancel') }} {{ $t('post.comment.submit') }}
</ds-button> </base-button>
</ds-flex-item> </div>
<ds-flex-item :width="{ base: '40%', md: '20%', sm: '40%', xs: '40%' }">
<ds-button type="submit" :loading="loading" :disabled="disabled || errors" primary>
{{ $t('post.comment.submit') }}
</ds-button>
</ds-flex-item>
</ds-flex>
</ds-card> </ds-card>
</template> </template>
</ds-form> </ds-form>
@ -149,3 +143,17 @@ export default {
}, },
} }
</script> </script>
<style lang="scss">
.comment-form {
.buttons {
display: flex;
justify-content: flex-end;
margin: $space-small 0;
> .base-button {
margin-left: $space-x-small;
}
}
}
</style>

View File

@ -63,12 +63,7 @@ describe('CommentList.vue', () => {
it('displays a comments counter', () => { it('displays a comments counter', () => {
wrapper = Wrapper() wrapper = Wrapper()
expect(wrapper.find('span.ds-tag').text()).toEqual('1') expect(wrapper.find('.count').text()).toEqual('1')
})
it('displays a comments counter', () => {
wrapper = Wrapper()
expect(wrapper.find('span.ds-tag').text()).toEqual('1')
}) })
describe('scrollToAnchor mixin', () => { describe('scrollToAnchor mixin', () => {

View File

@ -1,9 +1,8 @@
<template> <template>
<div id="comments"> <div id="comments" class="comment-list">
<h3 style="margin-top: -10px;"> <h3 class="title">
<counter-icon icon="comments" :count="post.comments.length"> <counter-icon icon="comments" :count="post.comments.length" />
{{ $t('common.comment', null, 0) }} {{ $t('common.comment', null, 0) }}
</counter-icon>
</h3> </h3>
<ds-space margin-bottom="large" /> <ds-space margin-bottom="large" />
<div v-if="post.comments && post.comments.length" id="comments" class="comments"> <div v-if="post.comments && post.comments.length" id="comments" class="comments">
@ -55,3 +54,15 @@ export default {
}, },
} }
</script> </script>
<style lang="scss">
.comment-list {
> .title {
margin-top: 0;
> .counter-icon {
margin-right: $space-small;
}
}
}
</style>

View File

@ -46,7 +46,7 @@ describe('ContentMenu.vue', () => {
store, store,
localVue, localVue,
}) })
menuToggle = wrapper.find('.content-menu-trigger') menuToggle = wrapper.find('[data-test="content-menu-button"]')
menuToggle.trigger('click') menuToggle.trigger('click')
return wrapper return wrapper
} }

View File

@ -2,9 +2,14 @@
<dropdown class="content-menu" :placement="placement" offset="5"> <dropdown class="content-menu" :placement="placement" offset="5">
<template slot="default" slot-scope="{ toggleMenu }"> <template slot="default" slot-scope="{ toggleMenu }">
<slot name="button" :toggleMenu="toggleMenu"> <slot name="button" :toggleMenu="toggleMenu">
<ds-button class="content-menu-trigger" size="small" ghost @click.prevent="toggleMenu"> <base-button
<base-icon name="ellipsis-v" /> data-test="content-menu-button"
</ds-button> icon="ellipsis-v"
size="small"
circle
ghost
@click="toggleMenu"
/>
</slot> </slot>
</template> </template>
<div slot="popover" slot-scope="{ toggleMenu }" class="content-menu-popover"> <div slot="popover" slot-scope="{ toggleMenu }" class="content-menu-popover">

View File

@ -200,6 +200,7 @@ describe('ContributionForm.vue', () => {
imageUpload: null, imageUpload: null,
imageAspectRatio: null, imageAspectRatio: null,
image: null, image: null,
imageBlurred: false,
}, },
} }
postTitleInput = wrapper.find('.ds-input') postTitleInput = wrapper.find('.ds-input')
@ -257,7 +258,7 @@ describe('ContributionForm.vue', () => {
describe('cancel', () => { describe('cancel', () => {
it('calls $router.back() when cancel button clicked', () => { it('calls $router.back() when cancel button clicked', () => {
cancelBtn = wrapper.find('.cancel-button') cancelBtn = wrapper.find('[data-test="cancel-button"]')
cancelBtn.trigger('click') cancelBtn.trigger('click')
expect(mocks.$router.back).toHaveBeenCalledTimes(1) expect(mocks.$router.back).toHaveBeenCalledTimes(1)
}) })
@ -307,6 +308,7 @@ describe('ContributionForm.vue', () => {
name: 'Democracy & Politics', name: 'Democracy & Politics',
}, },
], ],
imageAspectRatio: 1,
}, },
} }
wrapper = Wrapper() wrapper = Wrapper()
@ -353,7 +355,7 @@ describe('ContributionForm.vue', () => {
categoryIds: ['cat12'], categoryIds: ['cat12'],
image, image,
imageUpload: null, imageUpload: null,
imageAspectRatio: null, imageAspectRatio: 1,
}, },
} }
}) })

View File

@ -10,6 +10,7 @@
<hc-teaser-image <hc-teaser-image
:contribution="contribution" :contribution="contribution"
@addTeaserImage="addTeaserImage" @addTeaserImage="addTeaserImage"
:class="{ '--blur-image': form.blurImage }"
@addImageAspectRatio="addImageAspectRatio" @addImageAspectRatio="addImageAspectRatio"
> >
<img <img
@ -18,7 +19,23 @@
:src="contribution.image | proxyApiUrl" :src="contribution.image | proxyApiUrl"
/> />
</hc-teaser-image> </hc-teaser-image>
<ds-card> <ds-card>
<div class="blur-toggle">
<label for="blur-img">{{ $t('contribution.inappropriatePicture') }}</label>
<input type="checkbox" id="blur-img" v-model="form.blurImage" />
<p>
<a
href="https://support.human-connection.org/kb/faq.php?id=113"
target="_blank"
class="link"
>
{{ $t('contribution.inappropriatePictureText') }}
<ds-icon name="question-circle" />
</a>
</p>
</div>
<ds-space /> <ds-space />
<client-only> <client-only>
<hc-user :user="currentUser" :trunc="35" /> <hc-user :user="currentUser" :trunc="35" />
@ -80,19 +97,15 @@
<ds-icon name="warning"></ds-icon> <ds-icon name="warning"></ds-icon>
</ds-chip> </ds-chip>
</ds-text> </ds-text>
<ds-space /> <ds-space />
<div slot="footer" style="text-align: right"> <div slot="footer" style="text-align: right">
<ds-button <base-button data-test="cancel-button" :disabled="loading" @click="$router.back()" danger>
class="cancel-button"
:disabled="loading"
ghost
@click.prevent="$router.back()"
>
{{ $t('actions.cancel') }} {{ $t('actions.cancel') }}
</ds-button> </base-button>
<ds-button type="submit" icon="check" :loading="loading" :disabled="errors" primary> <base-button type="submit" icon="check" :loading="loading" :disabled="errors" filled>
{{ $t('actions.save') }} {{ $t('actions.save') }}
</ds-button> </base-button>
</div> </div>
<ds-space margin-bottom="large" /> <ds-space margin-bottom="large" />
</ds-card> </ds-card>
@ -134,7 +147,9 @@ export default {
image: null, image: null,
language: null, language: null,
categoryIds: [], categoryIds: [],
blurImage: false,
} }
let id = null let id = null
let slug = null let slug = null
const form = { ...formDefaults } const form = { ...formDefaults }
@ -149,7 +164,10 @@ export default {
? languageOptions.find(o => this.contribution.language === o.value) ? languageOptions.find(o => this.contribution.language === o.value)
: null : null
form.categoryIds = this.categoryIds(this.contribution.categories) form.categoryIds = this.categoryIds(this.contribution.categories)
form.imageAspectRatio = this.contribution.imageAspectRatio
form.blurImage = this.contribution.imageBlurred
} }
return { return {
form, form,
formSchema: { formSchema: {
@ -167,6 +185,7 @@ export default {
}, },
}, },
language: { required: true }, language: { required: true },
blurImage: { required: false },
}, },
languageOptions, languageOptions,
id, id,
@ -175,6 +194,7 @@ export default {
users: [], users: [],
contentMin: 3, contentMin: 3,
hashtags: [], hashtags: [],
elem: null,
} }
}, },
computed: { computed: {
@ -195,6 +215,7 @@ export default {
teaserImage, teaserImage,
imageAspectRatio, imageAspectRatio,
categoryIds, categoryIds,
blurImage,
} = this.form } = this.form
this.loading = true this.loading = true
this.$apollo this.$apollo
@ -208,6 +229,7 @@ export default {
language, language,
image, image,
imageUpload: teaserImage, imageUpload: teaserImage,
imageBlurred: blurImage,
imageAspectRatio, imageAspectRatio,
}, },
}) })
@ -273,28 +295,35 @@ export default {
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss">
.smallTag {
width: 100%;
position: relative;
left: 90%;
}
.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;
}
}
.contribution-form { .contribution-form {
.ds-card-image.--blur-image img {
filter: blur(32px);
}
.blur-toggle {
text-align: right;
> .link {
display: block;
}
}
.ds-chip { .ds-chip {
cursor: default; 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;
}
}
} }
</style> </style>

View File

@ -62,7 +62,13 @@
/> />
</ds-flex-item> </ds-flex-item>
<ds-flex-item :width="{ base: '100%', sm: '100%', md: '100%', lg: 1 }"> <ds-flex-item :width="{ base: '100%', sm: '100%', md: '100%', lg: 1 }">
<ds-button icon="trash" danger :disabled="!deleteEnabled" @click="handleSubmit"> <ds-button
icon="trash"
danger
filled
:disabled="!deleteEnabled"
@click="handleSubmit"
>
{{ $t('settings.deleteUserAccount.name') }} {{ $t('settings.deleteUserAccount.name') }}
</ds-button> </ds-button>
</ds-flex-item> </ds-flex-item>

View File

@ -32,12 +32,12 @@ describe('DonationInfo.vue', () => {
it('displays a call to action button', () => { it('displays a call to action button', () => {
expect( expect(
Wrapper() Wrapper()
.find('.ds-button') .find('.base-button')
.text(), .text(),
).toBe('donations.donate-now') ).toBe('donations.donate-now')
}) })
it('creates a title from the current month and a translation string', () => { it.skip('creates a title from the current month and a translation string', () => {
mocks.$t = jest.fn(() => 'Spenden für') mocks.$t = jest.fn(() => 'Spenden für')
expect(Wrapper().vm.title).toBe('Spenden für Dezember') expect(Wrapper().vm.title).toBe('Spenden für Dezember')
}) })
@ -49,7 +49,7 @@ describe('DonationInfo.vue', () => {
}) })
describe('given german locale', () => { describe('given german locale', () => {
it('creates a label from the given amounts and a translation string', () => { it.skip('creates a label from the given amounts and a translation string', () => {
expect(mocks.$t).toBeCalledWith( expect(mocks.$t).toBeCalledWith(
'donations.amount-of-total', 'donations.amount-of-total',
expect.objectContaining({ expect.objectContaining({
@ -65,7 +65,7 @@ describe('DonationInfo.vue', () => {
mocks.$i18n.locale = () => 'en' mocks.$i18n.locale = () => 'en'
}) })
it('creates a label from the given amounts and a translation string', () => { it.skip('creates a label from the given amounts and a translation string', () => {
expect(mocks.$t).toBeCalledWith( expect(mocks.$t).toBeCalledWith(
'donations.amount-of-total', 'donations.amount-of-total',
expect.objectContaining({ expect.objectContaining({

View File

@ -2,7 +2,7 @@
<div class="donation-info"> <div class="donation-info">
<progress-bar :title="title" :label="label" :goal="goal" :progress="progress" /> <progress-bar :title="title" :label="label" :goal="goal" :progress="progress" />
<a target="_blank" href="https://human-connection.org/spenden/"> <a target="_blank" href="https://human-connection.org/spenden/">
<ds-button primary>{{ $t('donations.donate-now') }}</ds-button> <base-button filled>{{ $t('donations.donate-now') }}</base-button>
</a> </a>
</div> </div>
</template> </template>

View File

@ -1,7 +1,7 @@
<template> <template>
<ds-button size="small" :ghost="!isActive" @click.prevent="onClick" :icon="icon"> <base-button size="small" circle :ghost="!isActive" @click="onClick" :icon="icon">
<span v-if="label">{{ label }}</span> <span v-if="label">{{ label }}</span>
</ds-button> </base-button>
</template> </template>
<script> <script>

View File

@ -1,30 +1,27 @@
<template> <template>
<ds-space margin-top="large" margin-bottom="xxx-small"> <ds-form
<ds-form class="enter-nonce"
v-model="formData" v-model="formData"
:schema="formSchema" :schema="formSchema"
@submit="handleSubmitVerify" @submit="handleSubmitVerify"
@input="handleInput" @input="handleInput"
@input-valid="handleInputValid" @input-valid="handleInputValid"
> >
<ds-input <ds-input
:placeholder="$t('components.enter-nonce.form.nonce')" :placeholder="$t('components.enter-nonce.form.nonce')"
model="nonce" model="nonce"
name="nonce" name="nonce"
id="nonce" id="nonce"
icon="question-circle" icon="question-circle"
/> />
<ds-space margin-botton="large"> <ds-text>
<ds-text> {{ $t('components.enter-nonce.form.description') }}
{{ $t('components.enter-nonce.form.description') }} </ds-text>
</ds-text> <base-button :disabled="disabled" filled name="submit" type="submit">
</ds-space> {{ $t('components.enter-nonce.form.next') }}
<ds-button :disabled="disabled" primary fullwidth name="submit" type="submit"> </base-button>
{{ $t('components.enter-nonce.form.next') }}
</ds-button>
</ds-form>
<slot></slot> <slot></slot>
</ds-space> </ds-form>
</template> </template>
<script> <script>
@ -64,3 +61,11 @@ export default {
}, },
} }
</script> </script>
<style lang="scss">
.enter-nonce {
display: flex;
flex-direction: column;
margin: $space-large 0 $space-xxx-small 0;
}
</style>

View File

@ -39,7 +39,7 @@ describe('FilterMenu.vue', () => {
describe('click "clear-search-button" button', () => { describe('click "clear-search-button" button', () => {
it('emits clearSearch', () => { it('emits clearSearch', () => {
wrapper.find({ name: 'clear-search-button' }).trigger('click') wrapper.find('[name="clear-search-button"]').trigger('click')
expect(wrapper.emitted().clearSearch).toHaveLength(1) expect(wrapper.emitted().clearSearch).toHaveLength(1)
}) })
}) })

View File

@ -6,15 +6,16 @@
</ds-flex-item> </ds-flex-item>
<ds-flex-item> <ds-flex-item>
<div class="filter-menu-buttons"> <div class="filter-menu-buttons">
<ds-button <base-button
name="clear-search-button"
icon="close"
circle
@click="clearSearch"
v-tooltip="{ v-tooltip="{
content: this.$t('filter-menu.clearSearch'), content: this.$t('filter-menu.clearSearch'),
placement: 'left', placement: 'left',
delay: { show: 500 }, delay: { show: 500 },
}" }"
name="clear-search-button"
icon="close"
@click="clearSearch"
/> />
</div> </div>
</ds-flex-item> </ds-flex-item>

View File

@ -12,10 +12,11 @@
<ds-flex> <ds-flex>
<ds-flex-item width="10%" /> <ds-flex-item width="10%" />
<ds-flex-item width="100%"> <ds-flex-item width="100%">
<ds-button <base-button
circle
icon="check" icon="check"
@click.stop.prevent="resetCategories" @click="resetCategories"
:primary="!filteredCategoryIds.length" :filled="!filteredCategoryIds.length"
/> />
<ds-flex-item> <ds-flex-item>
<label class="category-labels">{{ $t('filter-posts.categories.all') }}</label> <label class="category-labels">{{ $t('filter-posts.categories.all') }}</label>
@ -37,10 +38,11 @@
<ds-flex v-for="category in chunk[index - 1]" :key="category.id" class="categories-menu"> <ds-flex v-for="category in chunk[index - 1]" :key="category.id" class="categories-menu">
<ds-flex class="categories-menu"> <ds-flex class="categories-menu">
<ds-flex-item width="100%" class="categories-menu-item"> <ds-flex-item width="100%" class="categories-menu-item">
<ds-button <base-button
circle
:icon="category.icon" :icon="category.icon"
:primary="filteredCategoryIds.includes(category.id)" :filled="filteredCategoryIds.includes(category.id)"
@click.stop.prevent="toggleCategory(category.id)" @click="toggleCategory(category.id)"
/> />
<ds-space margin-bottom="small" /> <ds-space margin-bottom="small" />
</ds-flex-item> </ds-flex-item>

View File

@ -92,7 +92,7 @@ describe('FilterPosts.vue', () => {
it('starts with all categories button active', () => { it('starts with all categories button active', () => {
const wrapper = openFilterPosts() const wrapper = openFilterPosts()
allCategoriesButton = wrapper.findAll('button').at(1) allCategoriesButton = wrapper.findAll('button').at(1)
expect(allCategoriesButton.attributes().class).toContain('ds-button-primary') expect(allCategoriesButton.attributes().class).toContain('--filled')
}) })
it('calls TOGGLE_CATEGORY when clicked', () => { it('calls TOGGLE_CATEGORY when clicked', () => {
@ -111,35 +111,35 @@ describe('FilterPosts.vue', () => {
expect(mutations['posts/TOGGLE_LANGUAGE']).toHaveBeenCalledWith({}, 'en') expect(mutations['posts/TOGGLE_LANGUAGE']).toHaveBeenCalledWith({}, 'en')
}) })
it('sets category button attribute `primary` when corresponding category is filtered', () => { it('sets category button attribute `filled` when corresponding category is filtered', () => {
getters['posts/filteredCategoryIds'] = jest.fn(() => ['cat9']) getters['posts/filteredCategoryIds'] = jest.fn(() => ['cat9'])
const wrapper = openFilterPosts() const wrapper = openFilterPosts()
democracyAndPoliticsButton = wrapper.findAll('button').at(4) democracyAndPoliticsButton = wrapper.findAll('button').at(4)
expect(democracyAndPoliticsButton.attributes().class).toContain('ds-button-primary') expect(democracyAndPoliticsButton.attributes().class).toContain('--filled')
}) })
it('sets language button attribute `primary` when corresponding language is filtered', () => { it('sets language button attribute `filled` when corresponding language is filtered', () => {
getters['posts/filteredLanguageCodes'] = jest.fn(() => ['es']) getters['posts/filteredLanguageCodes'] = jest.fn(() => ['es'])
const wrapper = openFilterPosts() const wrapper = openFilterPosts()
spanishButton = wrapper spanishButton = wrapper
.findAll('button.language-buttons') .findAll('button.language-buttons')
.at(languages.findIndex(l => l.code === 'es')) .at(languages.findIndex(l => l.code === 'es'))
expect(spanishButton.attributes().class).toContain('ds-button-primary') expect(spanishButton.attributes().class).toContain('--filled')
}) })
it('sets "filter-by-followed-authors-only" button attribute `primary`', () => { it('sets "filter-by-followed" button attribute `filled`', () => {
getters['posts/filteredByUsersFollowed'] = jest.fn(() => true) getters['posts/filteredByUsersFollowed'] = jest.fn(() => true)
const wrapper = openFilterPosts() const wrapper = openFilterPosts()
expect( expect(wrapper.find('.base-button[data-test="filter-by-followed"]').classes('--filled')).toBe(
wrapper.find({ name: 'filter-by-followed-authors-only' }).classes('ds-button-primary'), true,
).toBe(true) )
}) })
describe('click "filter-by-followed-authors-only" button', () => { describe('click "filter-by-followed" button', () => {
let wrapper let wrapper
beforeEach(() => { beforeEach(() => {
wrapper = openFilterPosts() wrapper = openFilterPosts()
wrapper.find({ name: 'filter-by-followed-authors-only' }).trigger('click') wrapper.find('.base-button[data-test="filter-by-followed"]').trigger('click')
}) })
it('calls TOGGLE_FILTER_BY_FOLLOWED', () => { it('calls TOGGLE_FILTER_BY_FOLLOWED', () => {

View File

@ -1,15 +1,15 @@
<template> <template>
<dropdown ref="menu" :placement="placement" :offset="offset"> <dropdown ref="menu" :placement="placement" :offset="offset">
<ds-button <base-button
slot="default" slot="default"
icon="filter" icon="filter"
:primary="filterActive" :filled="filterActive"
:ghost="!filterActive" :ghost="!filterActive"
slot-scope="{ toggleMenu }" slot-scope="{ toggleMenu }"
@click.prevent="toggleMenu()" @click.prevent="toggleMenu()"
> >
<base-icon class="dropdown-arrow" name="angle-down" /> <base-icon class="dropdown-arrow" name="angle-down" />
</ds-button> </base-button>
<template slot="popover"> <template slot="popover">
<ds-container> <ds-container>
<categories-filter-menu-items :chunk="chunk" /> <categories-filter-menu-items :chunk="chunk" />

View File

@ -13,17 +13,18 @@
<ds-flex-item width="10%" /> <ds-flex-item width="10%" />
<ds-space margin-bottom="xx-small" /> <ds-space margin-bottom="xx-small" />
<ds-flex-item width="100%"> <ds-flex-item width="100%">
<div class="follow-button"> <div class="follow-filter-button">
<ds-button <base-button
data-test="filter-by-followed"
icon="user-plus"
circle
:filled="filteredByUsersFollowed"
@click="toggleFilteredByFollowed(user.id)"
v-tooltip="{ v-tooltip="{
content: this.$t('contribution.filterFollow'), content: this.$t('contribution.filterFollow'),
placement: 'left', placement: 'left',
delay: { show: 500 }, delay: { show: 500 },
}" }"
name="filter-by-followed-authors-only"
icon="user-plus"
:primary="filteredByUsersFollowed"
@click="toggleFilteredByFollowed(user.id)"
/> />
<ds-space margin-bottom="x-small" /> <ds-space margin-bottom="x-small" />
<ds-flex-item> <ds-flex-item>
@ -36,14 +37,9 @@
</ds-flex-item> </ds-flex-item>
<div v-for="emotion in emotionsArray" :key="emotion"> <div v-for="emotion in emotionsArray" :key="emotion">
<ds-flex-item :width="{ lg: '100%' }"> <ds-flex-item :width="{ lg: '100%' }">
<ds-button <base-button @click="toogleFilteredByEmotions(emotion)" class="emotions-buttons" circle>
size="large"
ghost
@click="toogleFilteredByEmotions(emotion)"
class="emotions-buttons"
>
<img :src="iconPath(emotion)" width="40" /> <img :src="iconPath(emotion)" width="40" />
</ds-button> </base-button>
<ds-space margin-bottom="x-small" /> <ds-space margin-bottom="x-small" />
<ds-flex-item class="emotions-mobile-space text-center"> <ds-flex-item class="emotions-mobile-space text-center">
<label class="emotions-label">{{ $t(`contribution.emotions-label.${emotion}`) }}</label> <label class="emotions-label">{{ $t(`contribution.emotions-label.${emotion}`) }}</label>
@ -99,7 +95,7 @@ export default {
#filter-posts-header { #filter-posts-header {
text-align: center; text-align: center;
} }
.follow-button { .follow-filter-button {
float: left; float: left;
} }
} }

View File

@ -12,10 +12,11 @@
<ds-flex> <ds-flex>
<ds-flex-item width="10%" /> <ds-flex-item width="10%" />
<ds-flex-item width="100%"> <ds-flex-item width="100%">
<ds-button <base-button
icon="check" icon="check"
@click.stop.prevent="resetLanguages" circle
:primary="!filteredLanguageCodes.length" :filled="!filteredLanguageCodes.length"
@click="resetLanguages"
/> />
<ds-flex-item> <ds-flex-item>
<label class="language-labels">{{ $t('filter-posts.language.all') }}</label> <label class="language-labels">{{ $t('filter-posts.language.all') }}</label>
@ -32,13 +33,14 @@
<ds-flex v-for="language in locales" :key="language.code" class="languages-menu"> <ds-flex v-for="language in locales" :key="language.code" class="languages-menu">
<ds-flex class="languages-menu"> <ds-flex class="languages-menu">
<ds-flex-item width="100%" class="language-menu-item"> <ds-flex-item width="100%" class="language-menu-item">
<ds-button <base-button
class="language-buttons" class="language-buttons"
:primary="filteredLanguageCodes.includes(language.code)" circle
@click.stop.prevent="toggleLanguage(language.code)" :filled="filteredLanguageCodes.includes(language.code)"
@click="toggleLanguage(language.code)"
> >
{{ language.code.toUpperCase() }} {{ language.code.toUpperCase() }}
</ds-button> </base-button>
<ds-space margin-bottom="small" /> <ds-space margin-bottom="small" />
</ds-flex-item> </ds-flex-item>
<ds-flex> <ds-flex>

View File

@ -1,17 +1,17 @@
<template> <template>
<ds-button <base-button
class="follow-button"
:disabled="disabled || !followId" :disabled="disabled || !followId"
:loading="loading" :loading="loading"
:icon="icon" :icon="icon"
:primary="isFollowed && !hovered" :filled="isFollowed && !hovered"
:danger="isFollowed && hovered" :danger="isFollowed && hovered"
fullwidth
@mouseenter.native="onHover" @mouseenter.native="onHover"
@mouseleave.native="hovered = false" @mouseleave.native="hovered = false"
@click.prevent="toggle" @click.prevent="toggle"
> >
{{ label }} {{ label }}
</ds-button> </base-button>
</template> </template>
<script> <script>
@ -83,3 +83,10 @@ export default {
}, },
} }
</script> </script>
<style lang="scss">
.follow-button {
display: block;
width: 100%;
}
</style>

View File

@ -51,11 +51,10 @@ describe('LoginForm', () => {
it('dispatches login with form data', () => { it('dispatches login with form data', () => {
fillIn(Wrapper()) fillIn(Wrapper())
expect(storeMocks.actions['auth/login']).toHaveBeenCalledWith( expect(storeMocks.actions['auth/login']).toHaveBeenCalledWith(expect.any(Object), {
expect.any(Object), email: 'email@example.org',
{ email: 'email@example.org', password: '1234' }, password: '1234',
undefined, })
)
}) })
}) })
}) })

View File

@ -50,16 +50,9 @@
<ds-space margin-bottom="large"> <ds-space margin-bottom="large">
<nuxt-link to="/password-reset/request">{{ $t('login.forgotPassword') }}</nuxt-link> <nuxt-link to="/password-reset/request">{{ $t('login.forgotPassword') }}</nuxt-link>
</ds-space> </ds-space>
<ds-button <base-button :loading="pending" filled name="submit" type="submit" icon="sign-in">
:loading="pending"
primary
fullwidth
name="submit"
type="submit"
icon="sign-in"
>
{{ $t('login.login') }} {{ $t('login.login') }}
</ds-button> </base-button>
<ds-space margin-top="large" margin-bottom="x-small"> <ds-space margin-top="large" margin-bottom="x-small">
{{ $t('login.no-account') }} {{ $t('login.no-account') }}
<nuxt-link to="/registration/signup">{{ $t('login.register') }}</nuxt-link> <nuxt-link to="/registration/signup">{{ $t('login.register') }}</nuxt-link>
@ -113,6 +106,11 @@ export default {
} }
.login-card { .login-card {
position: relative; position: relative;
.base-button {
display: block;
width: 100%;
}
} }
.login-locale-switch { .login-locale-switch {
position: absolute; position: absolute;

View File

@ -10,11 +10,16 @@
<p v-html="message" /> <p v-html="message" />
<template slot="footer"> <template slot="footer">
<ds-button class="cancel" :icon="modalData.buttons.cancel.icon" @click="cancel"> <base-button
class="cancel"
:danger="!modalData.buttons.confirm.danger"
:icon="modalData.buttons.cancel.icon"
@click="cancel"
>
{{ $t(modalData.buttons.cancel.textIdent) }} {{ $t(modalData.buttons.cancel.textIdent) }}
</ds-button> </base-button>
<ds-button <base-button
:danger="modalData.buttons.confirm.danger" :danger="modalData.buttons.confirm.danger"
class="confirm" class="confirm"
:icon="modalData.buttons.confirm.icon" :icon="modalData.buttons.confirm.icon"
@ -22,7 +27,7 @@
@click="confirm" @click="confirm"
> >
{{ $t(modalData.buttons.confirm.textIdent) }} {{ $t(modalData.buttons.confirm.textIdent) }}
</ds-button> </base-button>
</template> </template>
</ds-modal> </ds-modal>
</template> </template>

View File

@ -4,11 +4,10 @@
<p v-html="message" /> <p v-html="message" />
<template slot="footer"> <template slot="footer">
<ds-button class="cancel" @click="cancel">{{ $t('disable.cancel') }}</ds-button> <base-button class="cancel" @click="cancel">{{ $t('disable.cancel') }}</base-button>
<base-button danger filled class="confirm" icon="exclamation-circle" @click="confirm">
<ds-button danger class="confirm" icon="exclamation-circle" @click="confirm">
{{ $t('disable.submit') }} {{ $t('disable.submit') }}
</ds-button> </base-button>
</template> </template>
</ds-modal> </ds-modal>
</template> </template>

View File

@ -29,12 +29,13 @@
</small> </small>
<ds-space /> <ds-space />
<template #footer> <template #footer>
<ds-button class="cancel" icon="close" @click="cancel"> <base-button class="cancel" icon="close" @click="cancel">
{{ $t('report.cancel') }} {{ $t('report.cancel') }}
</ds-button> </base-button>
<ds-button <base-button
danger danger
filled
class="confirm" class="confirm"
icon="exclamation-circle" icon="exclamation-circle"
:disabled="!form.reasonCategory" :disabled="!form.reasonCategory"
@ -42,7 +43,7 @@
@click="confirm" @click="confirm"
> >
{{ $t('report.submit') }} {{ $t('report.submit') }}
</ds-button> </base-button>
</template> </template>
</ds-modal> </ds-modal>
</template> </template>
@ -161,7 +162,7 @@ export default {
.ds-modal { .ds-modal {
max-width: 600px !important; max-width: 600px !important;
} }
.ds-radio-option:not(.ds-button) { .ds-radio-option {
width: 100% !important; width: 100% !important;
} }
.ds-radio-option-label { .ds-radio-option-label {

View File

@ -1,11 +1,11 @@
import { config, shallowMount } from '@vue/test-utils' import { config, mount } from '@vue/test-utils'
import NotificationMenu from './NotificationMenu' import NotificationMenu from './NotificationMenu'
const localVue = global.localVue const localVue = global.localVue
localVue.filter('truncate', string => string) localVue.filter('truncate', string => string)
config.stubs.dropdown = '<span class="dropdown"><slot /></span>' config.stubs.dropdown = '<span class="dropdown"><slot :toggleMenu="() => null" /></span>'
describe('NotificationMenu.vue', () => { describe('NotificationMenu.vue', () => {
let wrapper let wrapper
@ -22,9 +22,9 @@ describe('NotificationMenu.vue', () => {
} }
}) })
describe('shallowMount', () => { describe('mount', () => {
const Wrapper = () => { const Wrapper = () => {
return shallowMount(NotificationMenu, { return mount(NotificationMenu, {
data, data,
mocks, mocks,
localVue, localVue,
@ -33,7 +33,7 @@ describe('NotificationMenu.vue', () => {
it('counter displays 0', () => { it('counter displays 0', () => {
wrapper = Wrapper() wrapper = Wrapper()
expect(wrapper.find('ds-button-stub').text()).toEqual('0') expect(wrapper.find('.count').text()).toEqual('0')
}) })
it('no dropdown is rendered', () => { it('no dropdown is rendered', () => {
@ -67,12 +67,12 @@ describe('NotificationMenu.vue', () => {
it('counter displays 0', () => { it('counter displays 0', () => {
wrapper = Wrapper() wrapper = Wrapper()
expect(wrapper.find('ds-button-stub').text()).toEqual('0') expect(wrapper.find('.count').text()).toEqual('0')
}) })
it('button is not primary', () => { it('counter is not colored', () => {
wrapper = Wrapper() wrapper = Wrapper()
expect(wrapper.find('ds-button-stub').props('primary')).toBe(false) expect(wrapper.find('.count').classes()).toContain('--inactive')
}) })
}) })
@ -130,12 +130,12 @@ describe('NotificationMenu.vue', () => {
it('displays the number of unread notifications', () => { it('displays the number of unread notifications', () => {
wrapper = Wrapper() wrapper = Wrapper()
expect(wrapper.find('ds-button-stub').text()).toEqual('2') expect(wrapper.find('.count').text()).toEqual('2')
}) })
it('renders primary button', () => { it('renders the counter in red', () => {
wrapper = Wrapper() wrapper = Wrapper()
expect(wrapper.find('ds-button-stub').props('primary')).toBe(true) expect(wrapper.find('.count').classes()).toContain('--danger')
}) })
}) })
}) })

View File

@ -1,12 +1,12 @@
<template> <template>
<ds-button v-if="!notifications.length" class="notifications-menu" disabled icon="bell"> <base-button v-if="!notifications.length" class="notifications-menu" disabled ghost circle>
{{ unreadNotificationsCount }} <counter-icon icon="bell" :count="unreadNotificationsCount" danger />
</ds-button> </base-button>
<dropdown v-else class="notifications-menu" offset="8" :placement="placement"> <dropdown v-else class="notifications-menu" offset="8" :placement="placement">
<template slot="default" slot-scope="{ toggleMenu }"> <template slot="default" slot-scope="{ toggleMenu }">
<ds-button :primary="!!unreadNotificationsCount" icon="bell" @click.prevent="toggleMenu"> <base-button @click="toggleMenu" ghost circle>
{{ unreadNotificationsCount }} <counter-icon icon="bell" :count="unreadNotificationsCount" danger />
</ds-button> </base-button>
</template> </template>
<template slot="popover"> <template slot="popover">
<div class="notifications-menu-popover"> <div class="notifications-menu-popover">
@ -22,17 +22,20 @@
</template> </template>
<script> <script>
import Dropdown from '~/components/Dropdown'
import { NOTIFICATIONS_POLL_INTERVAL } from '~/constants/notifications' import { NOTIFICATIONS_POLL_INTERVAL } from '~/constants/notifications'
import { notificationQuery, markAsReadMutation } from '~/graphql/User' import { notificationQuery, markAsReadMutation } from '~/graphql/User'
import NotificationList from '../NotificationList/NotificationList'
import unionBy from 'lodash/unionBy' import unionBy from 'lodash/unionBy'
import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon'
import Dropdown from '~/components/Dropdown'
import NotificationList from '../NotificationList/NotificationList'
export default { export default {
name: 'NotificationMenu', name: 'NotificationMenu',
components: { components: {
NotificationList, CounterIcon,
Dropdown, Dropdown,
NotificationList,
}, },
data() { data() {
return { return {
@ -90,8 +93,13 @@ export default {
<style lang="scss"> <style lang="scss">
.notifications-menu { .notifications-menu {
flex-shrink: 0;
display: flex; display: flex;
align-items: center; align-items: center;
.base-button {
overflow: visible;
}
} }
.notifications-menu-popover { .notifications-menu-popover {

View File

@ -24,9 +24,9 @@
/> />
<password-strength :password="formData.password" /> <password-strength :password="formData.password" />
<ds-space margin-top="base"> <ds-space margin-top="base">
<ds-button :loading="loading" :disabled="errors" primary> <base-button :loading="loading" :disabled="errors" filled type="submit">
{{ $t('settings.security.change-password.button') }} {{ $t('settings.security.change-password.button') }}
</ds-button> </base-button>
</ds-space> </ds-space>
</template> </template>
</ds-form> </ds-form>

View File

@ -24,9 +24,9 @@
/> />
<password-strength :password="formData.password" /> <password-strength :password="formData.password" />
<ds-space margin-top="base" margin-bottom="xxx-small"> <ds-space margin-top="base" margin-bottom="xxx-small">
<ds-button :loading="$apollo.loading" :disabled="errors" primary> <base-button :loading="$apollo.loading" :disabled="errors" filled type="submit">
{{ $t('settings.security.change-password.button') }} {{ $t('settings.security.change-password.button') }}
</ds-button> </base-button>
</ds-space> </ds-space>
</template> </template>
</ds-form> </ds-form>

View File

@ -20,17 +20,16 @@
<ds-space margin-botton="large"> <ds-space margin-botton="large">
<ds-text align="left">{{ $t('components.password-reset.request.form.description') }}</ds-text> <ds-text align="left">{{ $t('components.password-reset.request.form.description') }}</ds-text>
</ds-space> </ds-space>
<ds-button <base-button
:disabled="disabled" :disabled="disabled"
:loading="$apollo.loading" :loading="$apollo.loading"
primary filled
fullwidth
name="submit" name="submit"
type="submit" type="submit"
icon="envelope" icon="envelope"
> >
{{ $t('components.password-reset.request.form.submit') }} {{ $t('components.password-reset.request.form.submit') }}
</ds-button> </base-button>
<slot></slot> <slot></slot>
</ds-form> </ds-form>
<div v-else> <div v-else>

View File

@ -2,7 +2,12 @@
<ds-card <ds-card
:lang="post.language" :lang="post.language"
:image="post.image | proxyApiUrl" :image="post.image | proxyApiUrl"
:class="{ 'post-card': true, 'disabled-content': post.disabled, 'post--pinned': isPinned }" :class="{
'post-card': true,
'disabled-content': post.disabled,
'--pinned': isPinned,
'--blur-image': post.imageBlurred,
}"
> >
<!-- Post Link Target --> <!-- Post Link Target -->
<nuxt-link <nuxt-link
@ -151,23 +156,26 @@ export default {
}, },
} }
</script> </script>
<style lang="scss">
<style scoped lang="scss">
.ds-card-image img {
width: 100%;
max-height: 2000px;
object-fit: contain;
-o-object-fit: cover;
object-fit: cover;
-o-object-position: center;
object-position: center;
}
.post-card { .post-card {
cursor: pointer; justify-content: space-between;
position: relative; position: relative;
z-index: 1; z-index: 1;
justify-content: space-between; 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 { > .ds-card-content {
flex-grow: 0; flex-grow: 0;
@ -184,6 +192,8 @@ export default {
} }
.content-menu { .content-menu {
position: relative;
z-index: $z-index-post-card-link;
display: inline-block; display: inline-block;
margin-left: $space-xx-small; margin-left: $space-xx-small;
margin-right: -$space-x-small; margin-right: -$space-x-small;
@ -200,8 +210,4 @@ export default {
text-indent: -999999px; text-indent: -999999px;
} }
} }
.post--pinned {
border: 1px solid $color-warning;
}
</style> </style>

View File

@ -102,10 +102,11 @@
v-html="$t('components.registration.signup.form.no-political')" v-html="$t('components.registration.signup.form.no-political')"
></label> ></label>
</ds-text> </ds-text>
<ds-button <base-button
style="float: right;" style="float: right;"
icon="check" icon="check"
type="submit" type="submit"
filled
:loading="$apollo.loading" :loading="$apollo.loading"
:disabled=" :disabled="
errors || errors ||
@ -115,10 +116,9 @@
!noCommercial || !noCommercial ||
!noPolitical !noPolitical
" "
primary
> >
{{ $t('actions.save') }} {{ $t('actions.save') }}
</ds-button> </base-button>
</template> </template>
</ds-form> </ds-form>
</div> </div>

View File

@ -30,17 +30,16 @@
name="email" name="email"
icon="envelope" icon="envelope"
/> />
<ds-button <base-button
:disabled="disabled" :disabled="disabled"
:loading="$apollo.loading" :loading="$apollo.loading"
primary filled
fullwidth
name="submit" name="submit"
type="submit" type="submit"
icon="envelope" icon="envelope"
> >
{{ $t('components.registration.signup.form.submit') }} {{ $t('components.registration.signup.form.submit') }}
</ds-button> </base-button>
<slot></slot> <slot></slot>
</ds-form> </ds-form>
</ds-space> </ds-space>

View File

@ -4,11 +4,10 @@
<p v-html="message" /> <p v-html="message" />
<template slot="footer"> <template slot="footer">
<ds-button class="cancel" @click="cancel">{{ $t('release.cancel') }}</ds-button> <base-button class="cancel" @click="cancel">{{ $t('release.cancel') }}</base-button>
<base-button danger filled class="confirm" icon="exclamation-circle" @click="confirm">
<ds-button danger class="confirm" icon="exclamation-circle" @click="confirm">
{{ $t('release.submit') }} {{ $t('release.submit') }}
</ds-button> </base-button>
</template> </template>
</ds-modal> </ds-modal>
</template> </template>

View File

@ -1,140 +0,0 @@
import { mount } from '@vue/test-utils'
import SearchInput from './SearchInput.vue'
const localVue = global.localVue
localVue.filter('truncate', () => 'truncated string')
localVue.filter('dateTime', () => Date.now)
describe('SearchInput.vue', () => {
let mocks
let propsData
beforeEach(() => {
propsData = {}
})
describe('mount', () => {
const Wrapper = () => {
mocks = {
$t: () => {},
}
return mount(SearchInput, { mocks, localVue, propsData })
}
it('renders', () => {
expect(Wrapper().is('div')).toBe(true)
})
it('has id "nav-search"', () => {
expect(Wrapper().contains('#nav-search')).toBe(true)
})
it('defaults to an empty value', () => {
expect(Wrapper().vm.value).toBe('')
})
it('defaults to id "nav-search"', () => {
expect(Wrapper().vm.id).toBe('nav-search')
})
it('default to a 300 millisecond delay from the time the user stops typing to when the search starts', () => {
expect(Wrapper().vm.delay).toEqual(300)
})
it('defaults to an empty array as results', () => {
expect(Wrapper().vm.results).toEqual([])
})
it('defaults to pending false, as in the search is not pending', () => {
expect(Wrapper().vm.pending).toBe(false)
})
it('accepts values as a string', () => {
propsData = { value: 'abc' }
const wrapper = Wrapper()
expect(wrapper.vm.value).toEqual('abc')
})
describe('testing custom functions', () => {
let select
let wrapper
beforeEach(() => {
wrapper = Wrapper()
select = wrapper.find('.ds-select')
select.trigger('focus')
select.element.value = 'abcd'
})
it('opens the select dropdown when focused on', () => {
expect(wrapper.vm.isOpen).toBe(true)
})
it('opens the select dropdown and blurs after focused on', () => {
select.trigger('blur')
expect(wrapper.vm.isOpen).toBe(false)
})
it('is clearable', () => {
select.trigger('input')
select.trigger('keyup.esc')
expect(wrapper.emitted().clear.length).toBe(1)
})
it('changes the unprocessedSearchInput as the value changes', () => {
select.trigger('input')
expect(wrapper.vm.unprocessedSearchInput).toBe('abcd')
})
it('searches for the term when enter is pressed', async () => {
select.trigger('input')
select.trigger('keyup.enter')
await expect(wrapper.emitted().search[0]).toEqual(['abcd'])
})
it('calls onDelete when the delete key is pressed', () => {
const spy = jest.spyOn(wrapper.vm, 'onDelete')
select.trigger('input')
select.trigger('keyup.delete')
expect(spy).toHaveBeenCalledTimes(1)
})
it('calls query when a user starts a search by pressing enter', () => {
const spy = jest.spyOn(wrapper.vm, 'query')
select.trigger('input')
select.trigger('keyup.enter')
expect(spy).toHaveBeenCalledWith('abcd')
})
it('calls onSelect when a user selects an item in the search dropdown menu', async () => {
// searched for term in the browser, copied the results from Vuex in Vue dev tools
propsData = {
results: [
{
__typename: 'Post',
author: {
__typename: 'User',
id: 'u5',
name: 'Trick',
slug: 'trick',
},
commentsCount: 0,
createdAt: '2019-03-13T11:00:20.835Z',
id: 'p10',
label: 'Eos aut illo omnis quis eaque et iure aut.',
shoutedCount: 0,
slug: 'eos-aut-illo-omnis-quis-eaque-et-iure-aut',
value: 'Eos aut illo omnis quis eaque et iure aut.',
},
],
}
wrapper = Wrapper()
select.trigger('input')
const results = wrapper.find('.ds-select-option')
results.trigger('click')
await expect(wrapper.emitted().select[0]).toEqual(propsData.results)
})
})
})
})

View File

@ -1,268 +0,0 @@
<template>
<div
class="search"
aria-label="search"
role="search"
:class="{
'is-active': isActive,
'is-open': isOpen,
}"
>
<div class="field">
<div class="control">
<a v-if="isActive" class="search-clear-btn" @click="clear">&nbsp;</a>
<ds-select
:id="id"
ref="input"
v-model="searchValue"
class="input"
name="search"
type="search"
icon="search"
label-prop="id"
:no-options-available="emptyText"
:icon-right="isActive ? 'close' : null"
:filter="item => item"
:options="results"
:auto-reset-search="!searchValue"
:placeholder="$t('search.placeholder')"
:loading="pending"
@keyup.enter.native="onEnter"
@focus.capture.native="onFocus"
@blur.capture.native="onBlur"
@keyup.delete.native="onDelete"
@keyup.esc.native="clear"
@input.exact="onSelect"
@input.native="handleInput"
@click.capture.native="isOpen = true"
>
<template slot="option" slot-scope="{ option }">
<ds-flex>
<ds-flex-item class="search-option-label">
<ds-text>{{ option.label | truncate(70) }}</ds-text>
</ds-flex-item>
<ds-flex-item class="search-option-meta" width="280px">
<ds-flex>
<ds-flex-item>
<ds-text size="small" color="softer" class="search-meta">
<span style="text-align: right;">
<b>{{ option.commentsCount }}</b>
<base-icon name="comments" />
</span>
<span style="width: 36px; display: inline-block; text-align: right;">
<b>{{ option.shoutedCount }}</b>
<base-icon name="bullhorn" />
</span>
</ds-text>
</ds-flex-item>
<ds-flex-item>
<ds-text size="small" color="softer" align="right">
{{ option.author.name | truncate(32) }} -
{{ option.createdAt | dateTime('dd.MM.yyyy') }}
</ds-text>
</ds-flex-item>
</ds-flex>
</ds-flex-item>
</ds-flex>
</template>
</ds-select>
</div>
</div>
</div>
</template>
<script>
import { isEmpty } from 'lodash'
export default {
name: 'SearchInput',
props: {
id: {
type: String,
default: 'nav-search',
},
value: {
type: String,
default: '',
},
results: {
type: Array,
default: () => [],
},
delay: {
type: Number,
default: 300,
},
pending: {
type: Boolean,
default: false,
},
},
data() {
return {
searchProcess: null,
isOpen: false,
lastSearchTerm: '',
unprocessedSearchInput: '',
searchValue: '',
}
},
computed: {
// #: Unused at the moment?
isActive() {
return !isEmpty(this.lastSearchTerm)
},
emptyText() {
return this.isActive && !this.pending ? this.$t('search.failed') : this.$t('search.hint')
},
},
methods: {
async query(value) {
if (isEmpty(value) || value.length < 3) {
this.clear()
return
}
this.$emit('search', value)
},
handleInput(e) {
clearTimeout(this.searchProcess)
const value = e.target ? e.target.value.trim() : ''
this.isOpen = true
this.unprocessedSearchInput = value
this.searchProcess = setTimeout(() => {
this.lastSearchTerm = value
this.query(value)
}, this.delay)
},
onSelect(item) {
this.isOpen = false
this.$emit('select', item)
this.$nextTick(() => {
this.searchValue = this.lastSearchTerm
})
},
onFocus(e) {
clearTimeout(this.searchProcess)
this.isOpen = true
},
onBlur(e) {
this.searchValue = this.lastSearchTerm
// this.$nextTick(() => {
// this.searchValue = this.lastSearchTerm
// })
this.isOpen = false
clearTimeout(this.searchProcess)
},
onDelete(e) {
clearTimeout(this.searchProcess)
const value = e.target ? e.target.value.trim() : ''
if (isEmpty(value)) {
this.clear()
} else {
this.handleInput(e)
}
},
/**
* TODO: on enter we should go to a dedicated seach page!?
*/
onEnter(e) {
// this.isOpen = false
clearTimeout(this.searchProcess)
if (!this.pending) {
// this.lastSearchTerm = this.unprocessedSearchInput
this.query(this.unprocessedSearchInput)
}
},
clear() {
this.$emit('clear')
clearTimeout(this.searchProcess)
this.isOpen = false
this.unprocessedSearchInput = ''
this.lastSearchTerm = ''
this.searchValue = ''
},
},
}
</script>
<style lang="scss">
.search {
display: flex;
align-self: center;
width: 100%;
position: relative;
$padding-left: $space-x-small;
.search-option-label {
align-self: center;
padding-left: $padding-left;
}
.search-option-meta {
align-self: center;
.ds-flex {
flex-direction: column;
}
}
&,
.ds-select-dropdown {
transition: box-shadow 100ms;
max-height: 70vh;
}
&.is-open {
.ds-select-dropdown {
box-shadow: $box-shadow-x-large;
}
}
.ds-select-dropdown-message {
opacity: 0.5;
padding-left: $padding-left;
}
.search-clear-btn {
right: 0;
z-index: 10;
position: absolute;
height: 100%;
width: 36px;
cursor: pointer;
}
.search-meta {
float: right;
padding-top: 2px;
white-space: nowrap;
word-wrap: none;
.base-icon {
vertical-align: sub;
}
}
.ds-select {
z-index: $z-index-dropdown + 1;
}
.ds-select-option-hover {
.ds-text-size-small,
.ds-text-size-small-x {
color: $text-color-soft;
}
}
.field {
width: 100%;
display: flex;
align-items: center;
}
.control {
width: 100%;
}
}
</style>

View File

@ -1,12 +1,11 @@
<template> <template>
<ds-space margin="xx-small" class="text-align-center"> <ds-space margin="xx-small" class="text-align-center">
<ds-button <base-button
:loading="loading" :loading="loading"
:disabled="disabled" :disabled="disabled"
:ghost="!shouted" :filled="shouted"
:primary="shouted"
size="x-large"
icon="bullhorn" icon="bullhorn"
circle
@click="toggle" @click="toggle"
/> />
<ds-space margin-bottom="xx-small" /> <ds-space margin-bottom="xx-small" />

View File

@ -9,10 +9,18 @@
@vdropzone-thumbnail="transformImage" @vdropzone-thumbnail="transformImage"
> >
<div class="crop-overlay" ref="cropperOverlay" v-show="showCropper"> <div class="crop-overlay" ref="cropperOverlay" v-show="showCropper">
<ds-button @click.stop.prevent="cropImage" class="crop-confirm" primary> <base-button @click="cropImage" class="crop-confirm" filled>
{{ $t('contribution.teaserImage.cropperConfirm') }} {{ $t('contribution.teaserImage.cropperConfirm') }}
</ds-button> </base-button>
<ds-button @click="cancelCrop" class="crop-cancel" icon="close"></ds-button> <base-button
class="crop-cancel"
icon="close"
size="small"
circle
danger
filled
@click="cancelCrop"
/>
</div> </div>
<div <div
:class="{ :class="{
@ -140,7 +148,7 @@ export default {
<style lang="scss"> <style lang="scss">
#postdropzone { #postdropzone {
width: 100%; width: 100%;
min-height: 500px; min-height: 400px;
background-color: $background-color-softest; background-color: $background-color-softest;
} }

View File

@ -0,0 +1,85 @@
import { storiesOf } from '@storybook/vue'
import helpers from '~/storybook/helpers'
import BaseButton from './BaseButton.vue'
storiesOf('Generic/BaseButton', module)
.addDecorator(helpers.layout)
.add('default', () => ({
components: { BaseButton },
template: `
<div>
<base-button>Click me</base-button>
<base-button disabled>Disabled</base-button>
<base-button loading>Loading</base-button>
</div>
`,
}))
.add('icon', () => ({
components: { BaseButton },
template: `
<div>
<base-button icon="edit">With Text</base-button>
<base-button icon="bullhorn" />
<base-button icon="trash" disabled />
<base-button icon="trash" loading />
</div>
`,
}))
.add('circle', () => ({
components: { BaseButton },
template: `
<div>
<base-button circle icon="eye" />
<base-button circle>EN</base-button>
<base-button circle disabled icon="eye-slash" />
<base-button circle loading icon="eye-slash" />
</div>
`,
}))
.add('danger', () => ({
components: { BaseButton },
template: `
<div>
<base-button danger>Danger</base-button>
<base-button danger disabled>Disabled</base-button>
<base-button danger loading>Loading</base-button>
</div>
`,
}))
.add('filled', () => ({
components: { BaseButton },
template: `
<div>
<base-button filled>Filled</base-button>
<base-button filled danger>Filled Danger</base-button>
<base-button filled disabled>Disabled</base-button>
<base-button filled loading>Loading</base-button>
</div>
`,
}))
.add('small', () => ({
components: { BaseButton },
template: `
<div>
<base-button size="small">Small</base-button>
<base-button size="small" circle>S</base-button>
</div>
`,
}))
.add('ghost', () => ({
// TODO: add documentation --> ghost button should only be used for very special occasions
// e.g. for the ContentMenu + for the EditorMenuBarButtons
components: { BaseButton },
template: `
<div>
<base-button size="small" icon="ellipsis-v" circle ghost />
</div>
`,
}))

View File

@ -0,0 +1,144 @@
<template>
<button
:class="buttonClass"
:disabled="loading"
:type="type"
@click.capture="event => $emit('click', event)"
>
<base-icon v-if="icon" :name="icon" />
<loading-spinner v-if="loading" />
<slot />
</button>
</template>
<script>
import LoadingSpinner from '~/components/_new/generic/LoadingSpinner/LoadingSpinner'
export default {
components: {
LoadingSpinner,
},
props: {
circle: {
type: Boolean,
default: false,
},
danger: {
type: Boolean,
default: false,
},
filled: {
type: Boolean,
default: false,
},
ghost: {
type: Boolean,
default: false,
},
icon: {
type: String,
},
loading: {
type: Boolean,
default: false,
},
size: {
type: String,
default: 'regular',
validator(value) {
return value.match(/(small|regular)/)
},
},
type: {
type: String,
default: 'button',
validator(value) {
return value.match(/(button|submit)/)
},
},
},
computed: {
buttonClass() {
let buttonClass = 'base-button'
if (this.$slots.default === undefined) buttonClass += ' --icon-only'
if (this.circle) buttonClass += ' --circle'
if (this.danger) buttonClass += ' --danger'
if (this.loading) buttonClass += ' --loading'
if (this.size === 'small') buttonClass += ' --small'
if (this.filled) buttonClass += ' --filled'
else if (this.ghost) buttonClass += ' --ghost'
return buttonClass
},
},
}
</script>
<style lang="scss">
@import '~/assets/_new/styles/mixins/buttonStates.scss';
.base-button {
@include buttonStates;
display: inline-flex;
align-items: center;
justify-content: center;
height: $size-button-base;
padding: 0 $space-x-small;
vertical-align: bottom;
border: $border-size-base solid;
border-radius: $border-radius-x-large;
overflow: hidden;
font-weight: $font-weight-bold;
cursor: pointer;
&.--danger {
@include buttonStates($color-scheme: danger);
}
&.--filled {
@include buttonStates($filled: true);
}
&.--danger.--filled {
@include buttonStates($color-scheme: danger, $filled: true);
}
&.--circle {
width: $size-button-base;
border-radius: 50%;
}
&.--ghost {
border: none;
}
&.--small {
height: $size-button-small;
font-size: $font-size-small;
&.--circle {
width: $size-button-small;
}
}
&:not(.--icon-only) > .base-icon {
margin-right: $space-xx-small;
}
&:disabled.--loading {
color: $color-neutral-80;
}
> .loading-spinner {
position: absolute;
height: $size-button-small;
color: $color-neutral-60;
}
&.--filled > .loading-spinner {
color: $color-neutral-100;
}
}
</style>

View File

@ -29,7 +29,7 @@ const iconStyles = `
font-size: 20px; font-size: 20px;
` `
storiesOf('BaseIcon', module) storiesOf('Generic/BaseIcon', module)
.addDecorator(helpers.layout) .addDecorator(helpers.layout)
.add('pure icon', () => ({ .add('pure icon', () => ({

View File

@ -37,6 +37,7 @@ export default {
<style lang="scss"> <style lang="scss">
.base-icon { .base-icon {
display: inline-flex; display: inline-flex;
vertical-align: bottom;
> .svg { > .svg {
height: 1.2em; height: 1.2em;

View File

@ -5,33 +5,41 @@ import BaseIcon from '../BaseIcon/BaseIcon'
const localVue = global.localVue const localVue = global.localVue
describe('CounterIcon.vue', () => { describe('CounterIcon.vue', () => {
let propsData, wrapper, tag let propsData, wrapper, count
const Wrapper = () => { const Wrapper = () => {
return mount(CounterIcon, { propsData, localVue }) return mount(CounterIcon, { propsData, localVue })
} }
describe('given a valid icon name and count', () => { describe('given a valid icon name and count below 100', () => {
beforeEach(() => { beforeEach(() => {
propsData = { icon: 'comments', count: 1 } propsData = { icon: 'comments', count: 42 }
wrapper = Wrapper() wrapper = Wrapper()
tag = wrapper.find('.ds-tag') count = wrapper.find('.count')
}) })
it('renders BaseIcon', () => { it('renders the icon', () => {
expect(wrapper.find(BaseIcon).exists()).toBe(true) expect(wrapper.find(BaseIcon).exists()).toBe(true)
}) })
it('renders the count', () => { it('renders the count', () => {
expect(tag.text()).toEqual('1') expect(count.text()).toEqual('42')
})
})
describe('given a valid icon name and count above 100', () => {
beforeEach(() => {
propsData = { icon: 'comments', count: 750 }
wrapper = Wrapper()
count = wrapper.find('.count')
}) })
it('uses a round tag', () => { it('renders the icon', () => {
expect(tag.classes()).toContain('ds-tag-round') expect(wrapper.find(BaseIcon).exists()).toBe(true)
}) })
it('uses a primary button', () => { it('renders the capped count with a plus', () => {
expect(tag.classes()).toContain('ds-tag-primary') expect(count.text()).toEqual('99+')
}) })
}) })
}) })

View File

@ -2,18 +2,33 @@ import { storiesOf } from '@storybook/vue'
import helpers from '~/storybook/helpers' import helpers from '~/storybook/helpers'
import CounterIcon from './CounterIcon.vue' import CounterIcon from './CounterIcon.vue'
storiesOf('CounterIcon', module) storiesOf('Generic/CounterIcon', module)
.addDecorator(helpers.layout) .addDecorator(helpers.layout)
.add('flag icon with button in slot position', () => ({
.add('default', () => ({
components: { CounterIcon }, components: { CounterIcon },
data() {
return { icon: 'flag', count: 3 }
},
template: ` template: `
<counter-icon icon="pizza" :count="count"> <counter-icon icon="flag" :count="3" />
<ds-button ghost primary> `,
Report Details }))
</ds-button>
</counter-icon> .add('high count', () => ({
components: { CounterIcon },
template: `
<counter-icon icon="comments" :count="150" />
`,
}))
.add('danger', () => ({
components: { CounterIcon },
template: `
<counter-icon icon="bell" :count="42" danger />
`,
}))
.add('count is 0', () => ({
components: { CounterIcon },
template: `
<counter-icon icon="bell" :count="0" />
`, `,
})) }))

View File

@ -1,29 +1,63 @@
<template> <template>
<span> <span class="counter-icon">
<base-icon :name="icon" /> <base-icon :name="icon" />
<ds-tag <span :class="counterClass">{{ cappedCount }}</span>
style="margin-top: -4px; margin-left: -12px; position: absolute;"
color="primary"
size="small"
round
>
{{ count }}
</ds-tag>
<span class="counter-icon-text">
<slot />
</span>
</span> </span>
</template> </template>
<script> <script>
export default { export default {
props: { props: {
icon: { type: String, required: true }, icon: { type: String, required: true },
count: { type: Number, required: true }, count: { type: Number, required: true },
danger: { type: Boolean, default: false },
},
computed: {
cappedCount() {
return this.count <= 99 ? this.count : '99+'
},
counterClass() {
let counterClass = 'count'
if (this.danger) counterClass += ' --danger'
if (this.count === 0) counterClass += ' --inactive'
return counterClass
},
}, },
} }
</script> </script>
<style lang="scss" scoped>
.counter-icon-text { <style lang="scss">
margin-left: $space-xx-small; .counter-icon {
position: relative;
> .count {
position: absolute;
top: -$space-xx-small;
right: 0;
display: inline-flex;
align-items: center;
justify-content: center;
height: $size-icon-base;
min-width: $size-icon-base;
padding: 3px; // magic number to center count
border-radius: 50%;
transform: translateX(50%);
color: $color-neutral-100;
background-color: $color-primary;
font-size: 10px; // magic number to center count
line-height: 1;
text-align: center;
&.--danger {
background-color: $color-danger;
}
&.--inactive {
background-color: $color-neutral-60;
}
}
} }
</style> </style>

View File

@ -0,0 +1,11 @@
import { storiesOf } from '@storybook/vue'
import helpers from '~/storybook/helpers'
import LoadingSpinner from './LoadingSpinner.vue'
storiesOf('Generic/LoadingSpinner', module)
.addDecorator(helpers.layout)
.add('default', () => ({
components: { LoadingSpinner },
template: '<loading-spinner />',
}))

View File

@ -0,0 +1,48 @@
<template>
<svg viewBox="0 0 50 50" class="loading-spinner">
<circle cx="25" cy="25" r="20" class="circle" />
</svg>
</template>
<script>
export default {
name: 'LoadingSpinner',
}
</script>
<style lang="scss">
.loading-spinner {
height: $size-button-base;
overflow: hidden;
stroke: currentColor;
animation: rotate 16s linear infinite;
> .circle {
fill: none;
stroke-width: 5;
stroke-linecap: round;
animation: dash 1.5s ease-in-out infinite;
}
}
@keyframes dash {
0% {
stroke-dasharray: 1, 150;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -35;
}
100% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -124;
}
}
@keyframes rotate {
100% {
transform: rotate(2160deg);
}
}
</style>

View File

@ -40,7 +40,7 @@ describe('ReportRow', () => {
beforeEach(() => { beforeEach(() => {
propsData = { ...propsData, report: reports[1] } propsData = { ...propsData, report: reports[1] }
wrapper = Wrapper() wrapper = Wrapper()
confirmButton = wrapper.find('.ds-button-danger') confirmButton = wrapper.find('.base-button.--danger')
}) })
it('renders a confirm button', () => { it('renders a confirm button', () => {

View File

@ -11,9 +11,9 @@
<span class="user-count"> <span class="user-count">
{{ $t('moderation.reports.numberOfUsers', { count: report.filed.length }) }} {{ $t('moderation.reports.numberOfUsers', { count: report.filed.length }) }}
</span> </span>
<ds-button size="small" @click="showFiledReports = !showFiledReports"> <base-button size="small" @click="showFiledReports = !showFiledReports">
{{ $t('moderation.reports.moreDetails') }} {{ $t('moderation.reports.moreDetails') }}
</ds-button> </base-button>
</td> </td>
<!-- Content Column --> <!-- Content Column -->
@ -61,16 +61,17 @@
<span v-if="report.closed" class="title"> <span v-if="report.closed" class="title">
{{ $t('moderation.reports.decided') }} {{ $t('moderation.reports.decided') }}
</span> </span>
<ds-button <base-button
v-else v-else
danger danger
filled
data-test="confirm" data-test="confirm"
size="small" size="small"
:icon="statusIconName" :icon="statusIconName"
@click="$emit('confirm-report')" @click="$emit('confirm-report')"
> >
{{ $t('moderation.reports.decideButton') }} {{ $t('moderation.reports.decideButton') }}
</ds-button> </base-button>
</td> </td>
</tr> </tr>

View File

@ -0,0 +1,64 @@
import { config, mount } from '@vue/test-utils'
import Vuex from 'vuex'
import SearchField from './SearchField.vue'
import SearchableInput from '~/components/generic/SearchableInput/SearchableInput'
import { searchResults } from '~/components/generic/SearchableInput/SearchableInput.story'
const localVue = global.localVue
localVue.filter('truncate', () => 'truncated string')
localVue.filter('dateTime', () => Date.now)
config.stubs['nuxt-link'] = '<span><slot /></span>'
describe('SearchField.vue', () => {
let mocks, wrapper, getters
beforeEach(() => {
mocks = {
$apollo: {
query: jest.fn(),
},
$t: jest.fn(string => string),
}
getters = { 'auth/isModerator': () => false }
wrapper = Wrapper()
})
const Wrapper = () => {
const store = new Vuex.Store({
getters,
})
return mount(SearchField, { mocks, localVue, store })
}
describe('mount', () => {
describe('Emitted events', () => {
let searchableInputComponent
beforeEach(() => {
searchableInputComponent = wrapper.find(SearchableInput)
})
describe('query event', () => {
it('calls an apollo query', () => {
searchableInputComponent.vm.$emit('query', 'abcd')
expect(mocks.$apollo.query).toHaveBeenCalledWith(
expect.objectContaining({ variables: { query: 'abcd' } }),
)
})
})
describe('clearSearch event', () => {
beforeEach(() => {
wrapper.setData({ searchResults, pending: true })
searchableInputComponent.vm.$emit('clearSearch')
})
it('clears searchResults', () => {
expect(wrapper.vm.searchResults).toEqual([])
})
it('set pending to false', () => {
expect(wrapper.vm.pending).toBe(false)
})
})
})
})
})

View File

@ -0,0 +1,51 @@
<template>
<searchable-input
data-test="search-field"
:loading="pending"
:options="searchResults"
@query="query"
@clearSearch="clear"
/>
</template>
<script>
import { findResourcesQuery } from '~/graphql/Search.js'
import SearchableInput from '~/components/generic/SearchableInput/SearchableInput.vue'
export default {
name: 'SearchField',
components: {
SearchableInput,
},
data() {
return {
pending: false,
searchResults: [],
}
},
methods: {
async query(value) {
this.pending = true
try {
const {
data: { findResources },
} = await this.$apollo.query({
query: findResourcesQuery,
variables: {
query: value,
},
})
this.searchResults = findResources
} catch (error) {
this.searchResults = []
} finally {
this.pending = false
}
},
clear() {
this.pending = false
this.searchResults = []
},
},
}
</script>

View File

@ -0,0 +1,27 @@
import { mount } from '@vue/test-utils'
import SearchHeading from './SearchHeading.vue'
const localVue = global.localVue
describe('SearchHeading.vue', () => {
let mocks, wrapper, propsData
beforeEach(() => {
mocks = {
$t: jest.fn(string => string),
}
propsData = {
resourceType: 'Post',
}
wrapper = Wrapper()
})
const Wrapper = () => {
return mount(SearchHeading, { mocks, localVue, propsData })
}
describe('mount', () => {
it('renders heading', () => {
expect(wrapper.text()).toMatch('search.heading.Post')
})
})
})

View File

@ -0,0 +1,26 @@
<template>
<ds-flex-item class="search-heading">
<ds-heading soft size="h5">
{{ $t(`search.heading.${resourceType}`) }}
</ds-heading>
</ds-flex-item>
</template>
<script>
export default {
name: 'SearchHeading',
props: {
resourceType: { type: String, required: true },
},
}
</script>
<style lang="scss">
.search-heading {
display: flex;
flex-wrap: wrap;
font-weight: bold;
cursor: default;
background-color: white;
margin: -8px;
padding: 8px;
}
</style>

View File

@ -0,0 +1,64 @@
import { shallowMount } from '@vue/test-utils'
import SearchPost from './SearchPost.vue'
const localVue = global.localVue
localVue.filter('dateTime', d => d)
describe('SearchPost.vue', () => {
let mocks, wrapper, propsData
beforeEach(() => {
mocks = {
$t: jest.fn(string => string),
}
propsData = {
option: {
title: 'Post Title',
commentsCount: 3,
shoutedCount: 6,
createdAt: '23.08.2019',
author: {
name: 'Post Author',
},
},
}
wrapper = Wrapper()
})
const Wrapper = () => {
return shallowMount(SearchPost, { mocks, localVue, propsData })
}
describe('shallowMount', () => {
it('renders post title', () => {
expect(wrapper.find('.search-option-label').text()).toMatch('Post Title')
})
it('renders post commentsCount', () => {
expect(
wrapper
.find('.search-post-meta')
.findAll('span')
.filter(item => item.text() === '3')
.exists(),
).toBe(true)
})
it('renders post shoutedCount', () => {
expect(
wrapper
.find('.search-post-meta')
.findAll('span')
.filter(item => item.text() === '6')
.exists(),
).toBe(true)
})
it('renders post author', () => {
expect(wrapper.find('.search-post-author').text()).toContain('Post Author')
})
it('renders post createdAt', () => {
expect(wrapper.find('.search-post-author').text()).toContain('23.08.2019')
})
})
})

View File

@ -0,0 +1,71 @@
<template>
<ds-flex class="search-post">
<ds-flex-item class="search-option-label">
<ds-text>{{ option.title | truncate(70) }}</ds-text>
</ds-flex-item>
<ds-flex-item class="search-option-meta" width="280px">
<ds-flex>
<ds-flex-item>
<ds-text size="small" color="softer" class="search-post-meta">
<span class="comments-count">
{{ option.commentsCount }}
<base-icon name="comments" />
</span>
<span class="shouted-count">
{{ option.shoutedCount }}
<base-icon name="bullhorn" />
</span>
</ds-text>
</ds-flex-item>
<ds-flex-item>
<ds-text size="small" color="softer" align="right" class="search-post-author">
{{ option.author.name | truncate(32) }} -
{{ option.createdAt | dateTime('dd.MM.yyyy') }}
</ds-text>
</ds-flex-item>
</ds-flex>
</ds-flex-item>
</ds-flex>
</template>
<script>
export default {
name: 'SearchPost',
props: {
option: { type: Object, required: true },
},
}
</script>
<style lang="scss">
.search-post {
width: 100%;
}
.search-option-label {
align-self: center;
padding-left: $space-x-small;
}
.search-option-meta {
align-self: center;
.ds-flex {
flex-direction: column;
}
}
.search-post-meta {
float: right;
padding-top: 2px;
white-space: nowrap;
word-wrap: none;
.base-icon {
vertical-align: sub;
}
}
.shouted-count {
width: 36px;
display: inline-block;
text-align: right;
font-weight: $font-weight-bold;
}
.comments-count {
text-align: right;
font-weight: $font-weight-bold;
}
</style>

Some files were not shown because too many files have changed in this diff Show More