Merge branch 'master' into button-migration-fixes

This commit is contained in:
Alina Beck 2020-01-15 14:35:05 +03:00
commit a45d9749c8
74 changed files with 6139 additions and 5391 deletions

1
.github/stale.yml vendored
View File

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

View File

@ -8,5 +8,4 @@
}
],
"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).
#### [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)
> 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)
- 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)
- 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)
- 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)
@ -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 #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)
- Tell github-linguists to ignore snapshots [`978347b`](https://github.com/Human-Connection/Human-Connection/commit/978347ba7b5a6aa1bc915ada972ffffa2816d37c)
- Lokalise: update of webapp/locales/ru.json [`906e851`](https://github.com/Human-Connection/Human-Connection/commit/906e8518bf060134150187fb1574ac50ffd502f6)
- Lokalise: update of webapp/locales/ru.json [`3e52ee0`](https://github.com/Human-Connection/Human-Connection/commit/3e52ee090c88c357b796895370d126f8bb5529f0)
- Move components to components/features [`2357028`](https://github.com/Human-Connection/Human-Connection/commit/235702867d97b44dac37f8059f9194e23ba7f47d)
- Basic Search Is Working For Users And Posts [`72e4d0a`](https://github.com/Human-Connection/Human-Connection/commit/72e4d0abbcb9abab07f3fd12876453eb1de5da4c)
- 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)

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)"
EXPOSE 4000

View File

@ -32,20 +32,20 @@
]
},
"dependencies": {
"@hapi/joi": "^16.1.8",
"@sentry/node": "^5.10.2",
"@hapi/joi": "^17.0.2",
"@sentry/node": "^5.11.0",
"apollo-cache-inmemory": "~1.6.5",
"apollo-client": "~2.6.8",
"apollo-link-context": "~1.0.19",
"apollo-link-http": "~1.5.16",
"apollo-server": "~2.9.13",
"apollo-server-express": "^2.9.7",
"apollo-server": "~2.9.16",
"apollo-server-express": "^2.9.16",
"babel-plugin-transform-runtime": "^6.23.0",
"bcryptjs": "~2.4.3",
"cheerio": "~1.0.0-rc.3",
"cors": "~2.8.5",
"cross-env": "~6.0.3",
"date-fns": "2.8.1",
"date-fns": "2.9.0",
"debug": "~4.1.1",
"dotenv": "~8.2.0",
"express": "^4.17.1",
@ -55,41 +55,41 @@
"graphql-iso-date": "~3.6.1",
"graphql-middleware": "~4.0.2",
"graphql-middleware-sentry": "^3.2.1",
"graphql-shield": "~7.0.4",
"graphql-shield": "~7.0.7",
"graphql-tag": "~2.10.1",
"helmet": "~3.21.2",
"jsonwebtoken": "~8.5.1",
"linkifyjs": "~2.1.8",
"lodash": "~4.17.14",
"merge-graphql-schemas": "^1.7.3",
"metascraper": "^5.8.9",
"metascraper-audio": "^5.8.10",
"metascraper-author": "^5.8.7",
"merge-graphql-schemas": "^1.7.6",
"metascraper": "^5.10.2",
"metascraper-audio": "^5.9.5",
"metascraper-author": "^5.9.5",
"metascraper-clearbit-logo": "^5.3.0",
"metascraper-date": "^5.8.7",
"metascraper-description": "^5.8.10",
"metascraper-image": "^5.8.10",
"metascraper-lang": "^5.8.10",
"metascraper-date": "^5.9.5",
"metascraper-description": "^5.9.5",
"metascraper-image": "^5.9.5",
"metascraper-lang": "^5.9.5",
"metascraper-lang-detector": "^4.10.2",
"metascraper-logo": "^5.8.10",
"metascraper-publisher": "^5.8.7",
"metascraper-soundcloud": "^5.8.10",
"metascraper-title": "^5.8.10",
"metascraper-url": "^5.8.7",
"metascraper-video": "^5.8.10",
"metascraper-youtube": "^5.8.10",
"metascraper-logo": "^5.9.5",
"metascraper-publisher": "^5.9.5",
"metascraper-soundcloud": "^5.9.5",
"metascraper-title": "^5.9.5",
"metascraper-url": "^5.9.5",
"metascraper-video": "^5.9.5",
"metascraper-youtube": "^5.9.5",
"minimatch": "^3.0.4",
"mustache": "^3.1.0",
"neo4j-driver": "~1.7.6",
"neo4j-graphql-js": "^2.10.2",
"neode": "^0.3.6",
"mustache": "^3.2.1",
"neo4j-driver": "^4.0.1",
"neo4j-graphql-js": "^2.11.5",
"neode": "^0.3.7",
"node-fetch": "~2.6.0",
"nodemailer": "^6.4.2",
"nodemailer-html-to-text": "^3.1.0",
"npm-run-all": "~4.1.5",
"request": "~2.88.0",
"sanitize-html": "~1.20.1",
"slug": "~1.1.0",
"slug": "~2.1.0",
"trunc-html": "~1.1.2",
"uuid": "~3.3.3",
"validator": "^12.1.0",
@ -97,24 +97,24 @@
"xregexp": "^4.2.4"
},
"devDependencies": {
"@babel/cli": "~7.7.5",
"@babel/core": "~7.7.5",
"@babel/node": "~7.7.4",
"@babel/plugin-proposal-throw-expressions": "^7.7.4",
"@babel/preset-env": "~7.7.6",
"@babel/register": "~7.7.0",
"apollo-server-testing": "~2.9.13",
"@babel/cli": "~7.8.3",
"@babel/core": "~7.8.3",
"@babel/node": "~7.8.0",
"@babel/plugin-proposal-throw-expressions": "^7.8.3",
"@babel/preset-env": "~7.8.3",
"@babel/register": "~7.8.3",
"apollo-server-testing": "~2.9.16",
"babel-core": "~7.0.0-0",
"babel-eslint": "~10.0.3",
"babel-jest": "~24.9.0",
"chai": "~4.2.0",
"cucumber": "~6.0.5",
"eslint": "~6.7.2",
"eslint-config-prettier": "~6.7.0",
"eslint": "~6.8.0",
"eslint-config-prettier": "~6.9.0",
"eslint-config-standard": "~14.1.0",
"eslint-plugin-import": "~2.19.1",
"eslint-plugin-jest": "~23.1.1",
"eslint-plugin-node": "~10.0.0",
"eslint-plugin-import": "~2.20.0",
"eslint-plugin-jest": "~23.6.0",
"eslint-plugin-node": "~11.0.0",
"eslint-plugin-prettier": "~3.1.2",
"eslint-plugin-promise": "~4.2.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 Neode from 'neode'
import models from '../models'

View File

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

View File

@ -85,6 +85,8 @@ export default shield(
Query: {
'*': deny,
findPosts: allow,
findUsers: allow,
findResources: allow,
embed: allow,
Category: 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(),
},
language: { type: 'string', allow: [null] },
imageBlurred: { type: 'boolean', default: false },
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')
export default function log(response) {
const { statement, counters, resultConsumedAfter, resultAvailableAfter } = response.summary
const { text, parameters } = statement
const { counters, resultConsumedAfter, resultAvailableAfter, query } = response.summary
const { text, parameters } = query
debugCypher('%s', text)
debugCypher('%o', parameters)
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 { neo4jgraphql } from 'neo4j-graphql-js'
import { isEmpty } from 'lodash'
import fileUpload from './fileUpload'
import { getBlockedUsers, getBlockedByUsers } from './users.js'
import { mergeWith, isArray, isEmpty } from 'lodash'
import { UserInputError } from 'apollo-server'
import Resolver from './helpers/Resolver'
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
}
import { filterForBlockedUsers } from './helpers/filterForBlockedUsers'
const maintainPinnedPosts = params => {
const pinnedPostFilter = { pinned: true }
@ -341,6 +318,7 @@ export default {
'language',
'pinnedAt',
'pinned',
'imageBlurred',
'imageAspectRatio',
],
hasMany: {

View File

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

View File

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

View File

@ -1,8 +1,8 @@
import { gql } from '../../helpers/jest'
import Factory from '../../seed/factories'
import { getNeode, getDriver } from '../../bootstrap/neo4j'
import { gql } from '../../../helpers/jest'
import Factory from '../../../seed/factories'
import { getNeode, getDriver } from '../../../bootstrap/neo4j'
import { createTestClient } from 'apollo-server-testing'
import createServer from '../../server'
import createServer from '../../../server'
const factory = Factory()
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 {
# Get a JWT Token for the given Email and password
login(email: String!, password: String!): String!

View File

@ -82,6 +82,7 @@ input _PostFilter {
emotions_none: _PostEMOTEDFilter
emotions_single: _PostEMOTEDFilter
emotions_every: _PostEMOTEDFilter
imageBlurred: Boolean
}
enum _PostOrdering {
@ -127,6 +128,7 @@ type Post {
createdAt: String
updatedAt: String
language: String
imageBlurred: Boolean
pinnedAt: String @cypher(
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
"""
)
tags: [Tag]! @relation(name: "TAGGED", direction: "OUT")
categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
@ -183,6 +184,7 @@ type Mutation {
language: String
categoryIds: [ID]
contentExcerpt: String
imageBlurred: Boolean
imageAspectRatio: Float
): Post
UpdatePost(
@ -196,6 +198,7 @@ type Mutation {
visibility: Visibility
language: String
categoryIds: [ID]
imageBlurred: Boolean
imageAspectRatio: Float
): Post
DeletePost(id: ID!): Post
@ -217,6 +220,7 @@ type Query {
createdAt: String
updatedAt: String
language: String
imageBlurred: Boolean
first: Int
offset: Int
orderBy: [_PostOrdering]
@ -226,4 +230,18 @@ type Query {
PostsEmotionsCountByEmotion(postId: ID!, data: _EMOTEDInput!): Int!
PostsEmotionsByCurrentUser(postId: ID!): [String]
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]
blockedUsers: [User]
isLoggedIn: Boolean!
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 {
@ -179,8 +192,7 @@ type Mutation {
termsAndConditionsAgreedAt: String
allowEmbedIframes: Boolean
showShoutsPublicly: Boolean
locale: String
locale: String
): User
DeleteUser(id: ID!, resource: [Deletable]): User

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,24 @@
import { When, Then } from "cypress-cucumber-preprocessor/steps";
When("I search for {string}", value => {
cy.get("#nav-search")
cy.get(".searchable-input .ds-select-search")
.focus()
.type(value);
});
Then("I should have one post in the select dropdown", () => {
cy.get(".input .ds-select-dropdown").should($li => {
Then("I should have one item in the select dropdown", () => {
cy.get(".searchable-input .ds-select-dropdown").should($li => {
expect($li).to.have.length(1);
});
});
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);
});
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 => {
@ -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 => {
cy.get("#nav-search")
cy.get(".searchable-input .ds-select-search")
.focus()
.type(value)
.type("{enter}", { force: true });
});
When("I type {string} and press escape", value => {
cy.get("#nav-search")
cy.get(".searchable-input .ds-select-search")
.focus()
.type(value)
.type("{esc}");
});
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", () => {
cy.get(".input .ds-select-dropdown ul li")
When("I select a post entry", () => {
cy.get(".searchable-input .search-post")
.first()
.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

@ -9,18 +9,23 @@ Feature: Search
| id | title | content |
| 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 |
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
Scenario: Search for specific words
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:
| title |
| 101 Essays that will change the way you think |
Scenario: Press enter starts search
When I type "Essa" and press Enter
Then I should have one post in the select dropdown
When I type "Es" and press Enter
Then I should have one item in the select dropdown
Then I should see the following posts in the select dropdown:
| title |
| 101 Essays that will change the way you think |
@ -31,11 +36,20 @@ Feature: Search
Scenario: Select entry goes to post
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
Scenario: Select dropdown content
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
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)"
ARG BUILD_COMMIT

View File

@ -12,19 +12,11 @@ do
sleep 1
done
shopt -s nullglob
for image in uploads/*; do
[ -e "$image" ] || continue
IMAGE_WIDTH=$( identify -format '%w' "$image" )
IMAGE_HEIGHT=$( identify -format '%h' "$image" )
IMAGE_ASPECT_RATIO=$(echo | awk "{ print ${IMAGE_WIDTH}/${IMAGE_HEIGHT}}")
echo "$image"
echo "$IMAGE_ASPECT_RATIO"
echo "
match (post:Post {image: '/"${image}"'})
set post.imageAspectRatio = "${IMAGE_ASPECT_RATIO}"
return post;
" | cypher-shell
done
echo "
CALL apoc.periodic.iterate('
CALL apoc.load.csv("out.csv") yield map as row return row
','
MATCH (post:Post) where post.image = row.image
set post.imageAspectRatio = row.aspectRatio
', {batchSize:10000, iterateList:true, parallel:true});
" | cypher-shell

View File

@ -2,7 +2,6 @@
ENV_FILE=$(dirname "$0")/.env
[[ -f "$ENV_FILE" ]] && source "$ENV_FILE"
if [ -z "$NEO4J_USERNAME" ] || [ -z "$NEO4J_PASSWORD" ]; then
echo "Please set NEO4J_USERNAME and NEO4J_PASSWORD environment variables."
echo "Setting up database constraints and indexes will probably fail because of authentication errors."
@ -21,7 +20,8 @@ CALL db.indexes();
' | cypher-shell
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 (c:Comment) ASSERT c.id IS UNIQUE;
CREATE CONSTRAINT ON (c:Category) ASSERT c.id IS UNIQUE;

View File

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

View File

@ -317,6 +317,7 @@ $z-index-page-submenu: 2500;
$z-index-page-header: 2000;
$z-index-page-sidebar: 1500;
$z-index-sticky: 100;
$z-index-post-card-link: 5;
/**
* @tokens Media Query

View File

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

View File

@ -56,7 +56,7 @@ export default {
margin-top: 0;
> .counter-icon {
margin-right: 12px;
margin-right: $space-small;
}
}
}

View File

@ -200,6 +200,7 @@ describe('ContributionForm.vue', () => {
imageUpload: null,
imageAspectRatio: null,
image: null,
imageBlurred: false,
},
}
postTitleInput = wrapper.find('.ds-input')
@ -307,6 +308,7 @@ describe('ContributionForm.vue', () => {
name: 'Democracy & Politics',
},
],
imageAspectRatio: 1,
},
}
wrapper = Wrapper()
@ -353,7 +355,7 @@ describe('ContributionForm.vue', () => {
categoryIds: ['cat12'],
image,
imageUpload: null,
imageAspectRatio: null,
imageAspectRatio: 1,
},
}
})

View File

@ -10,6 +10,7 @@
<hc-teaser-image
:contribution="contribution"
@addTeaserImage="addTeaserImage"
:class="{ '--blur-image': form.blurImage }"
@addImageAspectRatio="addImageAspectRatio"
>
<img
@ -18,7 +19,23 @@
:src="contribution.image | proxyApiUrl"
/>
</hc-teaser-image>
<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 />
<client-only>
<hc-user :user="currentUser" :trunc="35" />
@ -80,6 +97,7 @@
<ds-icon name="warning"></ds-icon>
</ds-chip>
</ds-text>
<ds-space />
<div slot="footer" style="text-align: right">
<base-button data-test="cancel-button" :disabled="loading" @click="$router.back()" danger>
@ -129,7 +147,9 @@ export default {
image: null,
language: null,
categoryIds: [],
blurImage: false,
}
let id = null
let slug = null
const form = { ...formDefaults }
@ -144,7 +164,10 @@ export default {
? languageOptions.find(o => this.contribution.language === o.value)
: null
form.categoryIds = this.categoryIds(this.contribution.categories)
form.imageAspectRatio = this.contribution.imageAspectRatio
form.blurImage = this.contribution.imageBlurred
}
return {
form,
formSchema: {
@ -162,6 +185,7 @@ export default {
},
},
language: { required: true },
blurImage: { required: false },
},
languageOptions,
id,
@ -170,6 +194,7 @@ export default {
users: [],
contentMin: 3,
hashtags: [],
elem: null,
}
},
computed: {
@ -190,6 +215,7 @@ export default {
teaserImage,
imageAspectRatio,
categoryIds,
blurImage,
} = this.form
this.loading = true
this.$apollo
@ -203,6 +229,7 @@ export default {
language,
image,
imageUpload: teaserImage,
imageBlurred: blurImage,
imageAspectRatio,
},
})
@ -268,28 +295,35 @@ export default {
}
</script>
<style lang="scss" scoped>
.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;
}
}
<style lang="scss">
.contribution-form {
.ds-card-image.--blur-image img {
filter: blur(32px);
}
.blur-toggle {
text-align: right;
> .link {
display: block;
}
}
.ds-chip {
cursor: default;
}
.post-title {
margin-top: $space-x-small;
margin-bottom: $space-xx-small;
input {
border: 0;
font-size: $font-size-x-large;
font-weight: bold;
padding-left: 0;
padding-right: 0;
}
}
}
</style>

View File

@ -37,7 +37,7 @@ describe('DonationInfo.vue', () => {
).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')
expect(Wrapper().vm.title).toBe('Spenden für Dezember')
})
@ -49,7 +49,7 @@ describe('DonationInfo.vue', () => {
})
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(
'donations.amount-of-total',
expect.objectContaining({
@ -65,7 +65,7 @@ describe('DonationInfo.vue', () => {
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(
'donations.amount-of-total',
expect.objectContaining({

View File

@ -127,19 +127,19 @@ describe('FilterPosts.vue', () => {
expect(spanishButton.attributes().class).toContain('--filled')
})
it('sets "filter-by-followed-authors-only" button attribute `filled`', () => {
it('sets "filter-by-followed" button attribute `filled`', () => {
getters['posts/filteredByUsersFollowed'] = jest.fn(() => true)
const wrapper = openFilterPosts()
expect(
wrapper.find('.base-button[name="filter-by-followed-authors-only"]').classes('--filled'),
).toBe(true)
expect(wrapper.find('.base-button[data-test="filter-by-followed"]').classes('--filled')).toBe(
true,
)
})
describe('click "filter-by-followed-authors-only" button', () => {
describe('click "filter-by-followed" button', () => {
let wrapper
beforeEach(() => {
wrapper = openFilterPosts()
wrapper.find('.base-button[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', () => {

View File

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

View File

@ -1,4 +1,4 @@
import { config, shallowMount } from '@vue/test-utils'
import { config, mount } from '@vue/test-utils'
import MasonryGridItem from './MasonryGridItem'
const localVue = global.localVue
@ -8,24 +8,39 @@ config.stubs['ds-grid-item'] = '<span><slot /></span>'
describe('MasonryGridItem', () => {
let wrapper
beforeEach(() => {
wrapper = shallowMount(MasonryGridItem, { localVue })
wrapper.vm.$parent.$emit = jest.fn()
})
describe('given an imageAspectRatio', () => {
it('sets the initial rowSpan to 13 when the ratio is higher than 1.3', () => {
const propsData = { imageAspectRatio: 2 }
wrapper = mount(MasonryGridItem, { localVue, propsData })
it('emits "calculating-item-height" when starting calculation', async () => {
wrapper.vm.calculateItemHeight()
await wrapper.vm.$nextTick()
expect(wrapper.vm.rowSpan).toBe(13)
})
const firstCallArgument = wrapper.vm.$parent.$emit.mock.calls[0][0]
expect(firstCallArgument).toBe('calculating-item-height')
})
it('sets the initial rowSpan to 15 when the ratio is between 1.3 and 1', () => {
const propsData = { imageAspectRatio: 1.1 }
wrapper = mount(MasonryGridItem, { localVue, propsData })
it('emits "finished-calculating-item-height" after the calculation', async () => {
wrapper.vm.calculateItemHeight()
await wrapper.vm.$nextTick()
expect(wrapper.vm.rowSpan).toBe(15)
})
const secondCallArgument = wrapper.vm.$parent.$emit.mock.calls[1][0]
expect(secondCallArgument).toBe('finished-calculating-item-height')
it('sets the initial rowSpan to 18 when the ratio is between 1 and 0.7', () => {
const propsData = { imageAspectRatio: 0.7 }
wrapper = mount(MasonryGridItem, { localVue, propsData })
expect(wrapper.vm.rowSpan).toBe(18)
})
it('sets the initial rowSpan to 25 when the ratio is lower than 0.7', () => {
const propsData = { imageAspectRatio: 0.3 }
wrapper = mount(MasonryGridItem, { localVue, propsData })
expect(wrapper.vm.rowSpan).toBe(25)
})
describe('given no aspect ratio', () => {
it('sets the initial rowSpan to 8 when not given an imageAspectRatio', () => {
wrapper = mount(MasonryGridItem, { localVue })
expect(wrapper.vm.rowSpan).toBe(8)
})
})
})
})

View File

@ -5,6 +5,16 @@
</template>
<script>
const landscapeRatio = 1.3
const squareRatio = 1
const portraitRatio = 0.7
const getRowSpan = aspectRatio => {
if (aspectRatio >= landscapeRatio) return 13
else if (aspectRatio >= squareRatio) return 15
else if (aspectRatio >= portraitRatio) return 18
else return 25
}
export default {
props: {
imageAspectRatio: {
@ -14,7 +24,7 @@ export default {
},
data() {
return {
rowSpan: 10,
rowSpan: this.imageAspectRatio ? getRowSpan(this.imageAspectRatio) : 8,
}
},
methods: {
@ -34,13 +44,7 @@ export default {
},
},
mounted() {
const image = this.$el.querySelector('img')
if (image) {
image.onload = () => this.calculateItemHeight()
} else {
// use timeout to make sure layout is set up before calculation
setTimeout(() => this.calculateItemHeight(), 0)
}
this.calculateItemHeight()
},
}
</script>

View File

@ -2,7 +2,12 @@
<ds-card
:lang="post.language"
: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 -->
<nuxt-link
@ -99,6 +104,14 @@ export default {
default: () => {},
},
},
mounted() {
const width = this.$el.offsetWidth
const height = Math.min(width / this.post.imageAspectRatio, 2000)
const imageElement = this.$el.querySelector('.ds-card-image')
if (imageElement) {
imageElement.style.height = `${height}px`
}
},
computed: {
...mapGetters({
user: 'auth/user',
@ -143,23 +156,26 @@ export default {
},
}
</script>
<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;
}
<style lang="scss">
.post-card {
cursor: pointer;
justify-content: space-between;
position: relative;
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 {
flex-grow: 0;
@ -177,7 +193,7 @@ export default {
.content-menu {
position: relative;
z-index: 99;
z-index: $z-index-post-card-link;
display: inline-block;
margin-left: $space-xx-small;
margin-right: -$space-x-small;
@ -194,8 +210,4 @@ export default {
text-indent: -999999px;
}
}
.post--pinned {
border: 1px solid $color-warning;
}
</style>

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

@ -148,7 +148,7 @@ export default {
<style lang="scss">
#postdropzone {
width: 100%;
min-height: 500px;
min-height: 400px;
background-color: $background-color-softest;
}

View File

@ -61,7 +61,7 @@ export default {
buttonClass() {
let buttonClass = 'base-button'
if (this.$slots.default == null) buttonClass += ' --icon-only'
if (this.$slots.default === undefined) buttonClass += ' --icon-only'
if (this.circle) buttonClass += ' --circle'
if (this.danger) buttonClass += ' --danger'
if (this.loading) buttonClass += ' --loading'

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>

View File

@ -0,0 +1,115 @@
import { config, mount } from '@vue/test-utils'
import Vuex from 'vuex'
import Vue from 'vue'
import SearchableInput from './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('SearchableInput.vue', () => {
let mocks, propsData, getters, wrapper
beforeEach(() => {
propsData = {}
mocks = {
$router: {
push: jest.fn(),
},
$t: jest.fn(string => string),
}
getters = { 'auth/isModerator': () => false }
wrapper = Wrapper()
})
const Wrapper = () => {
const store = new Vuex.Store({
getters,
})
return mount(SearchableInput, { mocks, localVue, propsData, store })
}
describe('mount', () => {
describe('testing custom functions', () => {
let select
beforeEach(() => {
select = wrapper.find('.ds-select')
select.trigger('focus')
select.element.value = 'abcd'
})
it('opens the select dropdown when focused on', () => {
expect(wrapper.find('.is-open').exists()).toBe(true)
})
it('opens the select dropdown and blurs after focused on', () => {
select.trigger('blur')
expect(wrapper.find('.is-open').exists()).toBe(false)
})
it('is clearable', () => {
select.trigger('input')
select.trigger('keyup.esc')
expect(wrapper.find('.is-open').exists()).toBe(false)
})
it('changes the unprocessedSearchInput as the value changes', () => {
select.trigger('input')
expect(select.element.value).toBe('abcd')
})
it('searches for the term when enter is pressed', async () => {
select.element.value = 'ab'
select.trigger('input')
select.trigger('keyup.enter')
await expect(wrapper.emitted().query[0]).toEqual(['ab'])
})
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)
})
describe('navigating to resource', () => {
beforeEach(() => {
propsData = { options: searchResults }
wrapper = Wrapper()
select = wrapper.find('.ds-select')
select.trigger('focus')
})
it('pushes to post page', async () => {
select.element.value = 'Post'
select.trigger('input')
const post = wrapper.find('.search-post')
post.trigger('click')
await Vue.nextTick().then(() => {
expect(mocks.$router.push).toHaveBeenCalledWith({
name: 'post-id-slug',
params: { id: 'post-by-jenny', slug: 'user-post-by-jenny' },
})
})
})
it("pushes to user's profile", async () => {
select.element.value = 'Bob'
select.trigger('input')
const users = wrapper.findAll('.userinfo')
const bob = users.filter(item => item.text() === '@bob-der-baumeister')
bob.trigger('click')
await Vue.nextTick().then(() => {
expect(mocks.$router.push).toHaveBeenCalledWith({
name: 'profile-id-slug',
params: { id: 'u2', slug: 'bob-der-baumeister' },
})
})
})
})
})
})
})

View File

@ -0,0 +1,115 @@
import { storiesOf } from '@storybook/vue'
import { withA11y } from '@storybook/addon-a11y'
import SearchableInput from './SearchableInput.vue'
import helpers from '~/storybook/helpers'
helpers.init()
export const searchResults = [
{
id: 'post-by-jenny',
__typename: 'Post',
slug: 'user-post-by-jenny',
title: 'User Post by Jenny',
value: 'User Post by Jenny',
shoutedCount: 0,
commentCount: 4,
createdAt: '2019-11-13T03:03:16.155Z',
author: {
id: 'u3',
name: 'Jenny Rostock',
slug: 'jenny-rostock',
},
},
{
id: 'f48f00a0-c412-432f-8334-4276a4e15d1c',
__typename: 'Post',
slug: 'eum-quos-est-molestiae-enim-magni-consequuntur-sed-commodi-eos',
title: 'Eum quos est molestiae enim magni consequuntur sed commodi eos.',
value: 'Eum quos est molestiae enim magni consequuntur sed commodi eos.',
shoutedCount: 0,
commentCount: 0,
createdAt: '2019-11-13T03:00:45.478Z',
author: {
id: 'u6',
name: 'Louie',
slug: 'louie',
},
},
{
id: 'p7',
__typename: 'Post',
slug: 'this-is-post-7',
title: 'This is post #7',
value: 'This is post #7',
shoutedCount: 1,
commentCount: 1,
createdAt: '2019-11-13T03:00:23.098Z',
author: {
id: 'u6',
name: 'Louie',
slug: 'louie',
},
},
{
id: 'p12',
__typename: 'Post',
slug: 'this-is-post-12',
title: 'This is post #12',
value: 'This is post #12',
shoutedCount: 0,
commentCount: 12,
createdAt: '2019-11-13T03:00:23.098Z',
author: {
id: 'u6',
name: 'Louie',
slug: 'louie',
},
},
{
id: 'u1',
__typename: 'User',
avatar:
'https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/db/dbc9e03ebcc384b920c31542af2d27dd8eea9dc2_full.jpg',
name: 'Peter Lustig',
slug: 'peter-lustig',
},
{
id: 'cdbca762-0632-4564-b646-415a0c42d8b8',
__typename: 'User',
avatar:
'https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/db/dbc9e03ebcc384b920c31542af2d27dd8eea9dc2_full.jpg',
name: 'Herbert Schultz',
slug: 'herbert-schultz',
},
{
id: 'u2',
__typename: 'User',
avatar:
'https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/db/dbc9e03ebcc384b920c31542af2d27dd8eea9dc2_full.jpg',
name: 'Bob der Baumeister',
slug: 'bob-der-baumeister',
},
{
id: '7b654f72-f4da-4315-8bed-39de0859754b',
__typename: 'User',
avatar:
'https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/db/dbc9e03ebcc384b920c31542af2d27dd8eea9dc2_full.jpg',
name: 'Tonya Mohr',
slug: 'tonya-mohr',
},
]
storiesOf('Search Field', module)
.addDecorator(withA11y)
.addDecorator(helpers.layout)
.add('test', () => ({
components: { SearchableInput },
store: helpers.store,
data: () => ({
searchResults,
}),
template: `
<searchable-input :options="searchResults" />
`,
}))

View File

@ -0,0 +1,227 @@
<template>
<div
class="searchable-input"
aria-label="search"
role="search"
:class="{
'is-active': isActive,
'is-open': isOpen,
}"
>
<div class="field">
<div class="control">
<ds-button v-if="isActive" icon="close" ghost class="search-clear-btn" @click="clear" />
<ds-select
type="search"
icon="search"
v-model="searchValue"
:id="id"
label-prop="id"
:icon-right="isActive ? 'close' : null"
:options="options"
:loading="loading"
:filter="item => item"
:no-options-available="emptyText"
:auto-reset-search="!searchValue"
:placeholder="$t('search.placeholder')"
@click.capture.native="isOpen = true"
@focus.capture.native="onFocus"
@input.native="handleInput"
@keyup.enter.native="onEnter"
@keyup.delete.native="onDelete"
@keyup.esc.native="clear"
@blur.capture.native="onBlur"
@input.exact="onSelect"
>
<template #option="{ option }">
<span v-if="isFirstOfType(option)" class="search-heading">
<search-heading :resource-type="option.__typename" />
</span>
<span
v-if="option.__typename === 'User'"
:class="{ 'option-with-heading': isFirstOfType(option), 'flex-span': true }"
>
<hc-user :user="option" :showPopover="false" />
</span>
<span
v-if="option.__typename === 'Post'"
:class="{ 'option-with-heading': isFirstOfType(option), 'flex-span': true }"
>
<search-post :option="option" />
</span>
</template>
</ds-select>
</div>
</div>
</div>
</template>
<script>
import { isEmpty } from 'lodash'
import SearchHeading from '~/components/generic/SearchHeading/SearchHeading.vue'
import SearchPost from '~/components/generic/SearchPost/SearchPost.vue'
import HcUser from '~/components/User/User.vue'
export default {
name: 'SearchableInput',
components: {
SearchHeading,
SearchPost,
HcUser,
},
props: {
id: { type: String },
loading: { type: Boolean, default: false },
options: { type: Array, default: () => [] },
},
data() {
return {
isOpen: false,
searchValue: '',
value: '',
unprocessedSearchInput: '',
searchProcess: null,
previousSearchTerm: '',
delay: 300,
}
},
computed: {
emptyText() {
return this.isActive && !this.pending ? this.$t('search.failed') : this.$t('search.hint')
},
isActive() {
return !isEmpty(this.previousSearchTerm)
},
},
methods: {
isFirstOfType(option) {
return (
this.options.findIndex(o => o === option) ===
this.options.findIndex(o => o.__typename === option.__typename)
)
},
onFocus(event) {
clearTimeout(this.searchProcess)
this.isOpen = true
},
handleInput(event) {
clearTimeout(this.searchProcess)
this.value = event.target ? event.target.value.replace(/\s+/g, ' ').trim() : ''
this.isOpen = true
this.unprocessedSearchInput = this.value
if (isEmpty(this.value) || this.value.replace(/\s+/g, '').length < 3) {
return
}
this.searchProcess = setTimeout(() => {
this.previousSearchTerm = this.value
this.$emit('query', this.value)
}, this.delay)
},
/**
* TODO: on enter we should go to a dedicated search page!?
*/
onEnter(event) {
this.isOpen = false
clearTimeout(this.searchProcess)
if (!this.pending) {
this.previousSearchTerm = this.unprocessedSearchInput
this.$emit('query', this.unprocessedSearchInput)
}
},
onDelete(event) {
clearTimeout(this.searchProcess)
const value = event.target ? event.target.value.trim() : ''
if (isEmpty(value)) {
this.clear()
} else {
this.handleInput(event)
}
},
clear() {
this.isOpen = false
this.unprocessedSearchInput = ''
this.previousSearchTerm = ''
this.searchValue = ''
this.$emit('clearSearch')
clearTimeout(this.searchProcess)
},
onBlur(event) {
this.searchValue = this.previousSearchTerm
this.isOpen = false
clearTimeout(this.searchProcess)
},
onSelect(item) {
this.isOpen = false
this.goToResource(item)
this.$nextTick(() => {
this.searchValue = this.previousSearchTerm
})
},
isPost(item) {
return item.__typename === 'Post'
},
goToResource(item) {
this.$nextTick(() => {
this.$router.push({
name: this.isPost(item) ? 'post-id-slug' : 'profile-id-slug',
params: { id: item.id, slug: item.slug },
})
})
},
},
}
</script>
<style lang="scss">
.searchable-input {
display: flex;
align-self: center;
width: 100%;
position: relative;
$padding-left: $space-x-small;
.option-with-heading {
margin-top: $space-x-small;
padding-top: $space-xx-small;
}
.flex-span {
display: flex;
flex-wrap: wrap;
}
.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;
}
.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

@ -28,6 +28,7 @@ export const userCountsFragment = gql`
contributionsCount
commentedCount
followedByCount
followingCount
followedByCurrentUser
}
`
@ -45,6 +46,10 @@ export const postFragment = gql`
slug
image
language
imageBlurred
author {
...user
}
pinnedAt
imageAspectRatio
}

View File

@ -9,6 +9,7 @@ export default () => {
$language: String
$categoryIds: [ID]
$imageUpload: Upload
$imageBlurred: Boolean
$imageAspectRatio: Float
) {
CreatePost(
@ -17,6 +18,7 @@ export default () => {
language: $language
categoryIds: $categoryIds
imageUpload: $imageUpload
imageBlurred: $imageBlurred
imageAspectRatio: $imageAspectRatio
) {
title
@ -24,6 +26,7 @@ export default () => {
content
contentExcerpt
language
imageBlurred
}
}
`,
@ -36,6 +39,7 @@ export default () => {
$imageUpload: Upload
$categoryIds: [ID]
$image: String
$imageBlurred: Boolean
$imageAspectRatio: Float
) {
UpdatePost(
@ -46,6 +50,7 @@ export default () => {
imageUpload: $imageUpload
categoryIds: $categoryIds
image: $image
imageBlurred: $imageBlurred
imageAspectRatio: $imageAspectRatio
) {
id
@ -54,6 +59,7 @@ export default () => {
content
contentExcerpt
language
imageBlurred
pinnedBy {
id
name

24
webapp/graphql/Search.js Normal file
View File

@ -0,0 +1,24 @@
import gql from 'graphql-tag'
import { userFragment, postFragment } from './Fragments'
export const findResourcesQuery = gql`
${userFragment}
${postFragment}
query($query: String!) {
findResources(query: $query, limit: 5) {
__typename
... on Post {
...post
commentsCount
shoutedCount
author {
...user
}
}
... on User {
...user
}
}
}
`

View File

@ -19,18 +19,10 @@
:width="{ base: '45%', sm: '45%', md: '45%', lg: '50%' }"
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
style="flex-shrink: 0; flex-grow: 1;"
id="nav-search-box"
v-if="isLoggedIn"
>
<div id="nav-search-box" v-if="isLoggedIn">
<search-input
id="nav-search"
:delay="300"
:pending="quickSearchPending"
:results="quickSearchResults"
@clear="quickSearchClear"
@search="value => quickSearch({ value })"
@select="goToPost"
/>
</div>
<search-field />
</ds-flex-item>
<ds-flex-item
v-if="isLoggedIn"
@ -89,9 +81,9 @@
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
import { mapGetters } from 'vuex'
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
import SearchInput from '~/components/SearchInput.vue'
import SearchField from '~/components/features/SearchField/SearchField.vue'
import Modal from '~/components/Modal'
import NotificationMenu from '~/components/NotificationMenu/NotificationMenu'
import seo from '~/mixins/seo'
@ -103,7 +95,7 @@ import AvatarMenu from '~/components/AvatarMenu/AvatarMenu'
export default {
components: {
LocaleSwitch,
SearchInput,
SearchField,
Modal,
NotificationMenu,
AvatarMenu,
@ -121,8 +113,6 @@ export default {
computed: {
...mapGetters({
isLoggedIn: 'auth/isLoggedIn',
quickSearchResults: 'search/quickResults',
quickSearchPending: 'search/quickPending',
}),
showFilterPostsDropdown() {
const [firstRoute] = this.$route.matched
@ -135,18 +125,6 @@ export default {
},
},
methods: {
...mapActions({
quickSearchClear: 'search/quickClear',
quickSearch: 'search/quickSearch',
}),
goToPost(item) {
this.$nextTick(() => {
this.$router.push({
name: 'post-id-slug',
params: { id: item.id, slug: item.slug },
})
})
},
toggleMobileMenuView() {
this.toggleMobileMenu = !this.toggleMobileMenu
},

View File

@ -451,7 +451,8 @@
"filterFollow": "Beiträge filtern von Usern denen ich folge",
"filterALL": "Alle Beiträge anzeigen",
"success": "Gespeichert!",
"languageSelectLabel": "Sprache",
"languageSelectLabel": "Sprache deines Beitrags",
"languageSelectText": "Sprache wählen",
"categories": {
"infoSelectedNoOfMaxCategories": "{chosen} von {max} Kategorien ausgewählt"
},
@ -484,8 +485,9 @@
},
"teaserImage": {
"cropperConfirm": "Bestätigen"
},
"languageSelectText": "Sprache wählen"
},
"inappropriatePicture" : "Dieses Bild kann für einige Menschen unangemessen sein.",
"inappropriatePictureText" : "Wann soll ein Foto versteckt werden"
},
"comment": {
"edit": "Kommentar bearbeiten",
@ -513,7 +515,11 @@
"search": {
"placeholder": "Suchen",
"hint": "Wonach suchst Du?",
"failed": "Nichts gefunden"
"failed": "Nichts gefunden",
"heading": {
"Post": "Beiträge",
"User": "Benutzer"
}
},
"components": {
"password-reset": {
@ -546,7 +552,7 @@
"unavailable": "Leider ist die öffentliche Registrierung von Benutzerkonten auf diesem Server derzeit nicht möglich.",
"title": "Mach mit bei Human Connection!",
"form": {
"description": "Um loszulegen, gib deine E-Mail Adresse ein:",
"description": "Um loszulegen, kannst du dich hier kostenfrei registrieren:",
"terms-and-condition": "Ich stimme den <a href=\"\/terms-and-conditions\"><ds-text bold color=\"primary\" > Nutzungsbedingungen<\/ds-text><\/a>zu.",
"data-privacy": "Ich habe die <a href=\"https:\/\/human-connection.org\/datenschutz\/\" target=\"_blank\"><ds-text bold color=\"primary\" >Datenschutzerklärung<\/ds-text><\/a> gelesen und verstanden",
"minimum-age": "Ich bin 18 Jahre oder älter.",

View File

@ -30,7 +30,7 @@
"unavailable": "Unfortunately, public registration of user accounts is not available right now on this server.",
"title": "Join Human Connection!",
"form": {
"description": "To get started, enter your e-mail address:",
"description": "To get started, you can register here for free:",
"terms-and-condition": "I confirm to the <a href=\"/terms-and-conditions\"><ds-text bold color=\"primary\" > Terms and conditions</ds-text></a>.",
"data-privacy": " I have read and understood the <a href=\"https://human-connection.org/datenschutz/\" target=\"_blank\"><ds-text bold color=\"primary\" >Privacy Statement</ds-text></a> ",
"minimum-age": "I'm 18 years or older.",
@ -199,7 +199,11 @@
"search": {
"placeholder": "Search",
"hint": "What are you searching for?",
"failed": "Nothing found"
"failed": "Nothing found",
"heading": {
"Post": "Posts",
"User": "Users"
}
},
"settings": {
"name": "Settings",
@ -702,7 +706,9 @@
},
"teaserImage": {
"cropperConfirm": "Confirm"
}
},
"inappropriatePicture" : "This image may be inappropriate for some people.",
"inappropriatePictureText" : "When should my picture be blurred"
},
"code-of-conduct": {
"subheader": "for the social network of the Human Connection gGmbH",
@ -802,4 +808,3 @@

File diff suppressed because it is too large Load Diff

View File

@ -59,7 +59,7 @@
"dependencies": {
"@human-connection/styleguide": "0.5.22",
"@nuxtjs/apollo": "^4.0.0-rc19",
"@nuxtjs/axios": "~5.8.0",
"@nuxtjs/axios": "~5.9.3",
"@nuxtjs/dotenv": "~1.4.1",
"@nuxtjs/pwa": "^3.0.0-beta.19",
"@nuxtjs/sentry": "^3.0.1",
@ -70,13 +70,13 @@
"cookie-universal-nuxt": "~2.1.0",
"cropperjs": "^1.5.5",
"cross-env": "~6.0.3",
"date-fns": "2.8.1",
"date-fns": "2.9.0",
"express": "~4.17.1",
"graphql": "~14.5.8",
"jsonwebtoken": "~8.5.1",
"linkify-it": "~2.2.0",
"node-fetch": "^2.6.0",
"nuxt": "~2.10.2",
"nuxt": "~2.11.0",
"nuxt-dropzone": "^1.0.4",
"nuxt-env": "~0.1.0",
"stack-utils": "^2.0.1",
@ -96,14 +96,14 @@
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@babel/core": "~7.7.5",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/preset-env": "~7.7.6",
"@storybook/addon-a11y": "^5.2.8",
"@storybook/addon-actions": "^5.2.8",
"@storybook/addon-notes": "^5.2.8",
"@storybook/vue": "~5.2.8",
"@vue/cli-shared-utils": "~4.1.1",
"@babel/core": "~7.8.3",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/preset-env": "~7.8.3",
"@storybook/addon-a11y": "^5.3.3",
"@storybook/addon-actions": "^5.3.3",
"@storybook/addon-notes": "^5.3.3",
"@storybook/vue": "~5.3.3",
"@vue/cli-shared-utils": "~4.1.2",
"@vue/eslint-config-prettier": "~6.0.0",
"@vue/server-test-utils": "~1.0.0-beta.30",
"@vue/test-utils": "~1.0.0-beta.29",
@ -115,18 +115,18 @@
"babel-plugin-require-context-hook": "^1.0.0",
"babel-preset-vue": "~2.0.2",
"core-js": "~2.6.10",
"css-loader": "~3.3.2",
"eslint": "~6.7.2",
"eslint-config-prettier": "~6.7.0",
"css-loader": "~3.4.2",
"eslint": "~6.8.0",
"eslint-config-prettier": "~6.9.0",
"eslint-config-standard": "~14.1.0",
"eslint-loader": "~3.0.3",
"eslint-plugin-import": "~2.19.1",
"eslint-plugin-jest": "~23.1.1",
"eslint-plugin-node": "~10.0.0",
"eslint-plugin-import": "~2.20.0",
"eslint-plugin-jest": "~23.6.0",
"eslint-plugin-node": "~11.0.0",
"eslint-plugin-prettier": "~3.1.2",
"eslint-plugin-promise": "~4.2.1",
"eslint-plugin-standard": "~4.0.1",
"eslint-plugin-vue": "~6.0.1",
"eslint-plugin-vue": "~6.1.2",
"faker": "^4.1.0",
"flush-promises": "^1.0.2",
"fuse.js": "^3.4.6",
@ -135,14 +135,14 @@
"mutation-observer": "^1.0.3",
"node-sass": "~4.13.0",
"prettier": "~1.19.1",
"sass-loader": "~8.0.0",
"storybook-design-token": "^0.4.1",
"sass-loader": "~8.0.2",
"storybook-design-token": "^0.5.0",
"storybook-vue-router": "^1.0.7",
"style-loader": "~0.23.1",
"style-resources-loader": "~1.3.2",
"style-resources-loader": "~1.3.3",
"vue-jest": "~3.0.5",
"vue-loader": "~15.8.3",
"vue-svg-loader": "~0.15.0",
"vue-template-compiler": "^2.6.10"
"vue-template-compiler": "^2.6.11"
}
}

View File

@ -21,10 +21,13 @@
</div>
</ds-grid-item>
<template v-if="hasResults">
<masonry-grid-item v-for="post in posts" :key="post.id">
<masonry-grid-item
v-for="post in posts"
:key="post.id"
:imageAspectRatio="post.imageAspectRatio"
>
<hc-post-card
:post="post"
:width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }"
@removePostFromList="deletePost"
@pinPost="pinPost"
@unpinPost="unpinPost"

View File

@ -4,9 +4,16 @@
:lang="post.language"
v-if="post && ready"
:image="post.image | proxyApiUrl"
:class="{ 'post-card': true, 'disabled-content': post.disabled }"
:class="{
'post-page': true,
'disabled-content': post.disabled,
'--blur-image': blurred,
}"
>
<ds-space margin-bottom="small" />
<aside v-show="post.imageBlurred" class="blur-toggle">
<img v-show="blurred" :src="post.image | proxyApiUrl" class="preview" />
<ds-button :icon="blurred ? 'eye' : 'eye-slash'" primary @click="blurred = !blurred" />
</aside>
<hc-user :user="post.author" :date-time="post.createdAt">
<template v-slot:dateTime>
<ds-text v-if="post.createdAt !== post.updatedAt">({{ $t('post.edited') }})</ds-text>
@ -38,6 +45,11 @@
:icon="category.icon"
:name="$t(`contribution.category.name.${category.slug}`)"
/>
<!-- Post language -->
<ds-tag v-if="post.language" class="category-tag language">
<base-icon name="globe" />
{{ post.language.toUpperCase() }}
</ds-tag>
</div>
<ds-space margin-bottom="small" />
<!-- Tags -->
@ -121,12 +133,14 @@ export default {
ready: false,
title: 'loading',
showNewCommentForm: true,
blurred: false,
}
},
watch: {
Post(post) {
this.post = post[0] || {}
this.title = this.post.title
this.blurred = this.post.imageBlurred
},
},
mounted() {
@ -202,49 +216,70 @@ export default {
},
}
</script>
<style lang="scss">
.page-name-post-id-slug {
.post-page {
&.--blur-image > .ds-card-image img {
filter: blur(22px);
}
.ds-card-content {
position: relative;
padding-top: 24px;
}
.blur-toggle {
position: absolute;
top: -80px;
right: 0;
display: flex;
align-items: center;
height: 80px;
padding: 12px;
.preview {
height: 100%;
margin-right: 12px;
}
}
.content-menu {
float: right;
margin-right: -$space-x-small;
margin-top: -$space-large;
}
.post-card {
margin: auto;
cursor: auto;
.comments {
margin-top: $space-small;
.comments {
.comment {
margin-top: $space-small;
.comment {
margin-top: $space-small;
position: relative;
}
.ProseMirror {
min-height: 0px;
}
position: relative;
}
.ds-card-image {
img {
max-height: 2000px;
object-fit: contain;
object-position: center;
}
.ProseMirror {
min-height: 0px;
}
}
.ds-card-footer {
padding: 0;
.ds-card-image {
img {
max-height: 2000px;
object-fit: contain;
object-position: center;
}
}
.ds-section {
padding: $space-base;
}
.ds-card-footer {
padding: 0;
.ds-section {
padding: $space-base;
}
}
}
@media only screen and (max-width: 960px) {
.shout-button {
float: left;

View File

@ -19,7 +19,11 @@
<h3>{{ $t('post.moreInfo.titleOfRelatedContributionsSection') }}</h3>
<ds-section>
<masonry-grid v-if="post.relatedContributions && post.relatedContributions.length">
<masonry-grid-item v-for="relatedPost in post.relatedContributions" :key="relatedPost.id">
<masonry-grid-item
v-for="relatedPost in post.relatedContributions"
:key="relatedPost.id"
:imageAspectRatio="relatedPost.imageAspectRatio"
>
<hc-post-card
:post="relatedPost"
:width="{ base: '100%', lg: 1 }"

View File

@ -234,7 +234,11 @@
</ds-grid-item>
<template v-if="posts.length">
<masonry-grid-item v-for="post in posts" :key="post.id">
<masonry-grid-item
v-for="post in posts"
:key="post.id"
:imageAspectRatio="post.imageAspectRatio"
>
<hc-post-card
:post="post"
:width="{ base: '100%', md: '100%', xl: '50%' }"

View File

@ -1,86 +0,0 @@
import gql from 'graphql-tag'
import isString from 'lodash/isString'
export const state = () => {
return {
quickResults: [],
quickPending: false,
quickValue: '',
}
}
export const mutations = {
SET_QUICK_RESULTS(state, results) {
state.quickResults = results || []
state.quickPending = false
},
SET_QUICK_PENDING(state, pending) {
state.quickPending = pending
},
SET_QUICK_VALUE(state, value) {
state.quickValue = value
},
}
export const getters = {
quickResults(state) {
return state.quickResults
},
quickPending(state) {
return state.quickPending
},
quickValue(state) {
return state.quickValue
},
}
export const actions = {
async quickSearch({ commit, getters }, { value }) {
value = isString(value) ? value.trim() : ''
const lastVal = getters.quickValue
if (value.length < 3 || lastVal.toLowerCase() === value.toLowerCase()) {
return
}
commit('SET_QUICK_VALUE', value)
commit('SET_QUICK_PENDING', true)
await this.app.apolloProvider.defaultClient
.query({
query: gql`
query findPosts($query: String!, $filter: _PostFilter) {
findPosts(query: $query, limit: 10, filter: $filter) {
id
slug
label: title
value: title
shoutedCount
createdAt
author {
id
name
slug
}
}
}
`,
variables: {
query: value.replace(/\s/g, '~ ') + '~',
filter: {},
},
})
.then(res => {
commit('SET_QUICK_RESULTS', res.data.findPosts || [])
})
.catch(() => {
commit('SET_QUICK_RESULTS', [])
})
.finally(() => {
commit('SET_QUICK_PENDING', false)
})
return getters.quickResults
},
async quickClear({ commit }) {
commit('SET_QUICK_PENDING', false)
commit('SET_QUICK_RESULTS', [])
commit('SET_QUICK_VALUE', '')
},
}

File diff suppressed because it is too large Load Diff

1199
yarn.lock

File diff suppressed because it is too large Load Diff