Merge branch 'master' of github.com:Human-Connection/Human-Connection into 279-blacklist-users-content

This commit is contained in:
mattwr18 2020-01-13 21:22:58 +01:00
commit 84e475fd8a
41 changed files with 4784 additions and 2468 deletions

1
.github/stale.yml vendored
View File

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

View File

@ -4,6 +4,115 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [v0.2.1](https://github.com/Human-Connection/Human-Connection/compare/v0.2.0...v0.2.1)
> 10 January 2020
- 🍰 Search For Users [`#2262`](https://github.com/Human-Connection/Human-Connection/pull/2262)
- Use node LTS in production [`#2713`](https://github.com/Human-Connection/Human-Connection/pull/2713)
- build(deps): bump apollo-server from 2.9.15 to 2.9.16 in /backend [`#2718`](https://github.com/Human-Connection/Human-Connection/pull/2718)
- build(deps): bump neo4j-graphql-js from 2.11.4 to 2.11.5 in /backend [`#2715`](https://github.com/Human-Connection/Human-Connection/pull/2715)
- build(deps-dev): bump apollo-server-testing from 2.9.15 to 2.9.16 in /backend [`#2720`](https://github.com/Human-Connection/Human-Connection/pull/2720)
- build(deps): bump @hapi/joi from 17.0.0 to 17.0.2 in /backend [`#2719`](https://github.com/Human-Connection/Human-Connection/pull/2719)
- build(deps): bump apollo-server-express from 2.9.15 to 2.9.16 in /backend [`#2717`](https://github.com/Human-Connection/Human-Connection/pull/2717)
- build(deps): bump metascraper-url from 5.8.13 to 5.9.5 in /backend [`#2716`](https://github.com/Human-Connection/Human-Connection/pull/2716)
- build(deps): bump date-fns from 2.8.1 to 2.9.0 in /backend [`#2706`](https://github.com/Human-Connection/Human-Connection/pull/2706)
- build(deps): bump metascraper-lang from 5.8.13 to 5.9.5 in /backend [`#2703`](https://github.com/Human-Connection/Human-Connection/pull/2703)
- build(deps): bump date-fns from 2.8.1 to 2.9.0 in /webapp [`#2711`](https://github.com/Human-Connection/Human-Connection/pull/2711)
- build(deps): bump metascraper-logo from 5.8.13 to 5.9.5 in /backend [`#2697`](https://github.com/Human-Connection/Human-Connection/pull/2697)
- build(deps): bump metascraper-title from 5.8.13 to 5.9.5 in /backend [`#2694`](https://github.com/Human-Connection/Human-Connection/pull/2694)
- build(deps): bump metascraper-description from 5.8.15 to 5.9.5 in /backend [`#2690`](https://github.com/Human-Connection/Human-Connection/pull/2690)
- build(deps): bump node from 13.5.0-alpine to 13.6.0-alpine in /webapp [`#2708`](https://github.com/Human-Connection/Human-Connection/pull/2708)
- build(deps): bump @sentry/node from 5.10.2 to 5.11.0 in /backend [`#2709`](https://github.com/Human-Connection/Human-Connection/pull/2709)
- build(deps): bump metascraper-audio from 5.8.13 to 5.9.5 in /backend [`#2707`](https://github.com/Human-Connection/Human-Connection/pull/2707)
- build(deps): bump metascraper-image from 5.9.4 to 5.9.5 in /backend [`#2705`](https://github.com/Human-Connection/Human-Connection/pull/2705)
- build(deps): bump metascraper-youtube from 5.8.13 to 5.9.5 in /backend [`#2704`](https://github.com/Human-Connection/Human-Connection/pull/2704)
- build(deps-dev): bump date-fns from 2.8.1 to 2.9.0 [`#2702`](https://github.com/Human-Connection/Human-Connection/pull/2702)
- build(deps): bump metascraper-soundcloud from 5.9.0 to 5.9.5 in /backend [`#2693`](https://github.com/Human-Connection/Human-Connection/pull/2693)
- build(deps): bump metascraper-date from 5.8.13 to 5.9.5 in /backend [`#2698`](https://github.com/Human-Connection/Human-Connection/pull/2698)
- build(deps): bump neo4j-graphql-js from 2.11.3 to 2.11.4 in /backend [`#2696`](https://github.com/Human-Connection/Human-Connection/pull/2696)
- build(deps): bump metascraper-video from 5.8.13 to 5.9.5 in /backend [`#2695`](https://github.com/Human-Connection/Human-Connection/pull/2695)
- build(deps): bump metascraper-publisher from 5.8.13 to 5.9.5 in /backend [`#2692`](https://github.com/Human-Connection/Human-Connection/pull/2692)
- build(deps): bump metascraper-author from 5.8.13 to 5.9.5 in /backend [`#2691`](https://github.com/Human-Connection/Human-Connection/pull/2691)
- build(deps): bump metascraper from 5.9.4 to 5.9.5 in /backend [`#2689`](https://github.com/Human-Connection/Human-Connection/pull/2689)
- Changes Text For SignUp [`#2678`](https://github.com/Human-Connection/Human-Connection/pull/2678)
- Update de.json [`#2655`](https://github.com/Human-Connection/Human-Connection/pull/2655)
- build(deps): bump neode from 0.3.6 to 0.3.7 in /backend [`#2682`](https://github.com/Human-Connection/Human-Connection/pull/2682)
- Update neo4j-driver [`#2546`](https://github.com/Human-Connection/Human-Connection/pull/2546)
- build(deps): bump merge-graphql-schemas from 1.7.5 to 1.7.6 in /backend [`#2681`](https://github.com/Human-Connection/Human-Connection/pull/2681)
- build(deps): bump neo4j-graphql-js from 2.11.2 to 2.11.3 in /backend [`#2680`](https://github.com/Human-Connection/Human-Connection/pull/2680)
- build(deps-dev): bump neode from 0.3.6 to 0.3.7 [`#2679`](https://github.com/Human-Connection/Human-Connection/pull/2679)
- Parse xss before extracting mentions/hashtags [`#2674`](https://github.com/Human-Connection/Human-Connection/pull/2674)
- build(deps): bump metascraper-logo from 5.8.12 to 5.8.13 in /backend [`#2672`](https://github.com/Human-Connection/Human-Connection/pull/2672)
- build(deps): bump metascraper from 5.9.0 to 5.9.4 in /backend [`#2668`](https://github.com/Human-Connection/Human-Connection/pull/2668)
- build(deps-dev): bump eslint-plugin-jest from 23.2.0 to 23.3.0 in /webapp [`#2671`](https://github.com/Human-Connection/Human-Connection/pull/2671)
- build(deps-dev): bump css-loader from 3.4.0 to 3.4.1 in /webapp [`#2669`](https://github.com/Human-Connection/Human-Connection/pull/2669)
- build(deps): bump metascraper-image from 5.8.13 to 5.9.4 in /backend [`#2670`](https://github.com/Human-Connection/Human-Connection/pull/2670)
- build(deps-dev): bump apollo-server-testing from 2.9.14 to 2.9.15 in /backend [`#2667`](https://github.com/Human-Connection/Human-Connection/pull/2667)
- build(deps): bump neo4j-graphql-js from 2.11.0 to 2.11.2 in /backend [`#2666`](https://github.com/Human-Connection/Human-Connection/pull/2666)
- build(deps): bump metascraper-title from 5.8.12 to 5.8.13 in /backend [`#2665`](https://github.com/Human-Connection/Human-Connection/pull/2665)
- build(deps): bump @hapi/joi from 16.1.8 to 17.0.0 in /backend [`#2664`](https://github.com/Human-Connection/Human-Connection/pull/2664)
- build(deps-dev): bump cypress-file-upload from 3.5.1 to 3.5.3 [`#2663`](https://github.com/Human-Connection/Human-Connection/pull/2663)
- build(deps-dev): bump eslint-plugin-jest from 23.1.1 to 23.3.0 in /backend [`#2662`](https://github.com/Human-Connection/Human-Connection/pull/2662)
- build(deps-dev): bump eslint-config-prettier from 6.7.0 to 6.9.0 in /webapp [`#2632`](https://github.com/Human-Connection/Human-Connection/pull/2632)
- build(deps-dev): bump slug from 2.0.0 to 2.1.0 [`#2647`](https://github.com/Human-Connection/Human-Connection/pull/2647)
- build(deps): bump merge-graphql-schemas from 1.7.3 to 1.7.5 in /backend [`#2648`](https://github.com/Human-Connection/Human-Connection/pull/2648)
- build(deps): bump metascraper-url from 5.8.12 to 5.8.13 in /backend [`#2637`](https://github.com/Human-Connection/Human-Connection/pull/2637)
- build(deps): bump metascraper-publisher from 5.8.12 to 5.8.13 in /backend [`#2636`](https://github.com/Human-Connection/Human-Connection/pull/2636)
- build(deps-dev): bump eslint-plugin-jest from 23.1.1 to 23.2.0 in /webapp [`#2642`](https://github.com/Human-Connection/Human-Connection/pull/2642)
- build(deps): bump graphql-shield from 7.0.5 to 7.0.7 in /backend [`#2649`](https://github.com/Human-Connection/Human-Connection/pull/2649)
- build(deps-dev): bump eslint-plugin-node from 10.0.0 to 11.0.0 in /backend [`#2650`](https://github.com/Human-Connection/Human-Connection/pull/2650)
- build(deps-dev): bump eslint-config-prettier from 6.7.0 to 6.9.0 in /backend [`#2651`](https://github.com/Human-Connection/Human-Connection/pull/2651)
- build(deps): bump slug from 2.0.0 to 2.1.0 in /backend [`#2652`](https://github.com/Human-Connection/Human-Connection/pull/2652)
- build(deps): bump metascraper from 5.8.12 to 5.9.0 in /backend [`#2654`](https://github.com/Human-Connection/Human-Connection/pull/2654)
- build(deps): bump metascraper-description from 5.8.12 to 5.8.15 in /backend [`#2653`](https://github.com/Human-Connection/Human-Connection/pull/2653)
- build(deps): bump metascraper-author from 5.8.12 to 5.8.13 in /backend [`#2616`](https://github.com/Human-Connection/Human-Connection/pull/2616)
- build(deps): bump metascraper-lang from 5.8.12 to 5.8.13 in /backend [`#2618`](https://github.com/Human-Connection/Human-Connection/pull/2618)
- build(deps): bump apollo-server from 2.9.13 to 2.9.15 in /backend [`#2634`](https://github.com/Human-Connection/Human-Connection/pull/2634)
- build(deps): bump metascraper-soundcloud from 5.8.15 to 5.9.0 in /backend [`#2638`](https://github.com/Human-Connection/Human-Connection/pull/2638)
- build(deps): bump metascraper-video from 5.8.12 to 5.8.13 in /backend [`#2639`](https://github.com/Human-Connection/Human-Connection/pull/2639)
- build(deps): bump mustache from 3.2.0 to 3.2.1 in /backend [`#2640`](https://github.com/Human-Connection/Human-Connection/pull/2640)
- build(deps): bump slug from 1.1.0 to 2.0.0 in /backend [`#2641`](https://github.com/Human-Connection/Human-Connection/pull/2641)
- build(deps-dev): bump @vue/cli-shared-utils from 4.1.1 to 4.1.2 in /webapp [`#2643`](https://github.com/Human-Connection/Human-Connection/pull/2643)
- build(deps-dev): bump eslint-plugin-vue from 6.1.1 to 6.1.2 in /webapp [`#2644`](https://github.com/Human-Connection/Human-Connection/pull/2644)
- build(deps-dev): bump eslint-plugin-node from 10.0.0 to 11.0.0 in /webapp [`#2645`](https://github.com/Human-Connection/Human-Connection/pull/2645)
- build(deps-dev): bump @babel/preset-env from 7.7.6 to 7.7.7 in /webapp [`#2579`](https://github.com/Human-Connection/Human-Connection/pull/2579)
- build(deps): bump metascraper-soundcloud from 5.8.12 to 5.8.15 in /backend [`#2630`](https://github.com/Human-Connection/Human-Connection/pull/2630)
- build(deps-dev): bump eslint from 6.7.2 to 6.8.0 in /webapp [`#2617`](https://github.com/Human-Connection/Human-Connection/pull/2617)
- build(deps): bump metascraper-date from 5.8.12 to 5.8.13 in /backend [`#2615`](https://github.com/Human-Connection/Human-Connection/pull/2615)
- build(deps): bump metascraper-image from 5.8.12 to 5.8.13 in /backend [`#2614`](https://github.com/Human-Connection/Human-Connection/pull/2614)
- build(deps): bump metascraper-youtube from 5.8.12 to 5.8.13 in /backend [`#2612`](https://github.com/Human-Connection/Human-Connection/pull/2612)
- build(deps): bump metascraper-audio from 5.8.12 to 5.8.13 in /backend [`#2610`](https://github.com/Human-Connection/Human-Connection/pull/2610)
- 🍰 Added Language Tag For Posts [`#2627`](https://github.com/Human-Connection/Human-Connection/pull/2627)
- build(deps-dev): bump cypress-plugin-retries from 1.5.0 to 1.5.2 [`#2609`](https://github.com/Human-Connection/Human-Connection/pull/2609)
- build(deps-dev): bump eslint from 6.7.2 to 6.8.0 in /backend [`#2613`](https://github.com/Human-Connection/Human-Connection/pull/2613)
- remove accidently created ru.json in wrong place [`#2606`](https://github.com/Human-Connection/Human-Connection/pull/2606)
- build(deps): bump neo4j from 3.5.13-enterprise to 3.5.14-enterprise in /neo4j [`#2620`](https://github.com/Human-Connection/Human-Connection/pull/2620)
- Fixes 2603 [`#2619`](https://github.com/Human-Connection/Human-Connection/pull/2619)
- build(deps-dev): bump @babel/core from 7.7.5 to 7.7.7 in /webapp [`#2581`](https://github.com/Human-Connection/Human-Connection/pull/2581)
- build(deps-dev): bump slug from 1.1.0 to 2.0.0 [`#2621`](https://github.com/Human-Connection/Human-Connection/pull/2621)
- build(deps): [security] bump handlebars from 4.1.2 to 4.5.3 in /webapp [`#2624`](https://github.com/Human-Connection/Human-Connection/pull/2624)
- build(deps): [security] bump handlebars from 4.1.2 to 4.5.3 in /backend [`#2625`](https://github.com/Human-Connection/Human-Connection/pull/2625)
- build(deps-dev): bump cypress from 3.8.0 to 3.8.1 [`#2626`](https://github.com/Human-Connection/Human-Connection/pull/2626)
- build(deps-dev): bump eslint-plugin-vue from 6.0.1 to 6.1.1 in /webapp [`#2633`](https://github.com/Human-Connection/Human-Connection/pull/2633)
- build(deps-dev): bump @babel/register from 7.7.4 to 7.7.7 [`#2571`](https://github.com/Human-Connection/Human-Connection/pull/2571)
- build(deps): bump neo4j-graphql-js from 2.10.2 to 2.11.0 in /backend [`#2600`](https://github.com/Human-Connection/Human-Connection/pull/2600)
- build(deps-dev): bump @babel/core from 7.7.5 to 7.7.7 in /backend [`#2590`](https://github.com/Human-Connection/Human-Connection/pull/2590)
- build(deps): bump metascraper-url from 5.8.7 to 5.8.12 in /backend [`#2599`](https://github.com/Human-Connection/Human-Connection/pull/2599)
- build(deps): bump metascraper-lang from 5.8.10 to 5.8.12 in /backend [`#2598`](https://github.com/Human-Connection/Human-Connection/pull/2598)
- build(deps): bump metascraper-audio from 5.8.10 to 5.8.12 in /backend [`#2596`](https://github.com/Human-Connection/Human-Connection/pull/2596)
- build(deps): bump node from 13.4.0-alpine to 13.5.0-alpine in /webapp [`#2595`](https://github.com/Human-Connection/Human-Connection/pull/2595)
- build(deps-dev): bump storybook-design-token from 0.4.1 to 0.5.0 in /webapp [`#2594`](https://github.com/Human-Connection/Human-Connection/pull/2594)
- build(deps): bump graphql-shield from 7.0.4 to 7.0.5 in /backend [`#2593`](https://github.com/Human-Connection/Human-Connection/pull/2593)
- build(deps): bump metascraper-publisher from 5.8.7 to 5.8.12 in /backend [`#2592`](https://github.com/Human-Connection/Human-Connection/pull/2592)
- build(deps-dev): bump @babel/preset-env from 7.7.6 to 7.7.7 in /backend [`#2568`](https://github.com/Human-Connection/Human-Connection/pull/2568)
- Fix imageAspectRatio set to null UpdatePost [`#2588`](https://github.com/Human-Connection/Human-Connection/pull/2588)
- Update to version 0.2.0 [`#2584`](https://github.com/Human-Connection/Human-Connection/pull/2584)
- fixes #2659 [`#2659`](https://github.com/Human-Connection/Human-Connection/issues/2659)
- build(deps-dev): bump storybook-design-token in /webapp [`88d39c4`](https://github.com/Human-Connection/Human-Connection/commit/88d39c4a427cb86527b06201f3f5e96d53ac09a0)
- Specs for Searches [`bc3aa51`](https://github.com/Human-Connection/Human-Connection/commit/bc3aa519d0e7a6e0242ecd37d611fd1a3df385d0)
- build(deps): bump apollo-server-express in /backend [`84df7b5`](https://github.com/Human-Connection/Human-Connection/commit/84df7b5a0a4845ab44d19946d877aef79691d38e)
#### [v0.2.0](https://github.com/Human-Connection/Human-Connection/compare/v0.1.13...v0.2.0) #### [v0.2.0](https://github.com/Human-Connection/Human-Connection/compare/v0.1.13...v0.2.0)
> 19 December 2019 > 19 December 2019
@ -62,8 +171,8 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Fix: User.name is not non-nullable [`#2510`](https://github.com/Human-Connection/Human-Connection/pull/2510) - 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) - 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) - Lokalise: update of webapp/locales/ru.json [`b70ff73`](https://github.com/Human-Connection/Human-Connection/commit/b70ff73bba98d28494c55ed12161288b1efa1516)
- build(deps): bump apollo-server-express in /backend [`69d3107`](https://github.com/Human-Connection/Human-Connection/commit/69d3107cbcce8225dd14f7231936a597fba6105d) - Separate concerns in components [`d74d207`](https://github.com/Human-Connection/Human-Connection/commit/d74d2072ba41af6170d79d7dc2e24f9ebab15771)
- refactor: content menu [`71b2eac`](https://github.com/Human-Connection/Human-Connection/commit/71b2eac175e9d6e1a2bbba123490f281b7cb13f3) - Fix failing component tests [`b79c292`](https://github.com/Human-Connection/Human-Connection/commit/b79c292ef4f76b307131566c24d849c2e35c8089)
#### [v0.1.13](https://github.com/Human-Connection/Human-Connection/compare/v0.1.12...v0.1.13) #### [v0.1.13](https://github.com/Human-Connection/Human-Connection/compare/v0.1.12...v0.1.13)
@ -91,8 +200,8 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- build(deps): bump cookie-universal-nuxt from 2.0.19 to 2.1.0 in /webapp [`#2490`](https://github.com/Human-Connection/Human-Connection/pull/2490) - build(deps): bump cookie-universal-nuxt from 2.0.19 to 2.1.0 in /webapp [`#2490`](https://github.com/Human-Connection/Human-Connection/pull/2490)
- Update to version 0.1.12 [`#2483`](https://github.com/Human-Connection/Human-Connection/pull/2483) - Update to version 0.1.12 [`#2483`](https://github.com/Human-Connection/Human-Connection/pull/2483)
- Lokalise: update of locale/ru.json [`60b3035`](https://github.com/Human-Connection/Human-Connection/commit/60b3035a3d475cb481130c6fe94f2901711a4053) - Lokalise: update of locale/ru.json [`60b3035`](https://github.com/Human-Connection/Human-Connection/commit/60b3035a3d475cb481130c6fe94f2901711a4053)
- Fix search by adding result id [`ebc5cf3`](https://github.com/Human-Connection/Human-Connection/commit/ebc5cf392d92acf3a9e22c8967d02ea2cf6fd7fb)
- Write test/refactor tests/resolvers/middleware [`d375ebe`](https://github.com/Human-Connection/Human-Connection/commit/d375ebe7d90e3251b17f59ffba8fb1470923ebe8) - Write test/refactor tests/resolvers/middleware [`d375ebe`](https://github.com/Human-Connection/Human-Connection/commit/d375ebe7d90e3251b17f59ffba8fb1470923ebe8)
- refactor css, fix design issues [`5586335`](https://github.com/Human-Connection/Human-Connection/commit/5586335ed2b3474498e87b929f54d52562e44636)
#### [v0.1.12](https://github.com/Human-Connection/Human-Connection/compare/v0.1.10...v0.1.12) #### [v0.1.12](https://github.com/Human-Connection/Human-Connection/compare/v0.1.10...v0.1.12)
@ -258,8 +367,8 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Fix #2294 [`#2294`](https://github.com/Human-Connection/Human-Connection/issues/2294) - Fix #2294 [`#2294`](https://github.com/Human-Connection/Human-Connection/issues/2294)
- Merge pull request #2078 from Human-Connection/fix-2042-back-link [`#2042`](https://github.com/Human-Connection/Human-Connection/issues/2042) - Merge pull request #2078 from Human-Connection/fix-2042-back-link [`#2042`](https://github.com/Human-Connection/Human-Connection/issues/2042)
- Move components to components/features [`2357028`](https://github.com/Human-Connection/Human-Connection/commit/235702867d97b44dac37f8059f9194e23ba7f47d) - 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) - Add missing unit tests/refactor code [`b364065`](https://github.com/Human-Connection/Human-Connection/commit/b3640659bb608cc34edc6f2aca350f07dd2b9ce6)
- Add stories/specs for ReportList [`a59e72d`](https://github.com/Human-Connection/Human-Connection/commit/a59e72d8a8f491cb251e3e5acddea3b32144209b)
#### [v0.1.10](https://github.com/Human-Connection/Human-Connection/compare/v0.1.9...v0.1.10) #### [v0.1.10](https://github.com/Human-Connection/Human-Connection/compare/v0.1.9...v0.1.10)

View File

@ -1 +1 @@
0.2.0 0.2.1

View File

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

View File

@ -32,20 +32,20 @@
] ]
}, },
"dependencies": { "dependencies": {
"@hapi/joi": "^17.0.0", "@hapi/joi": "^17.0.2",
"@sentry/node": "^5.10.2", "@sentry/node": "^5.11.0",
"apollo-cache-inmemory": "~1.6.5", "apollo-cache-inmemory": "~1.6.5",
"apollo-client": "~2.6.8", "apollo-client": "~2.6.8",
"apollo-link-context": "~1.0.19", "apollo-link-context": "~1.0.19",
"apollo-link-http": "~1.5.16", "apollo-link-http": "~1.5.16",
"apollo-server": "~2.9.15", "apollo-server": "~2.9.16",
"apollo-server-express": "^2.9.14", "apollo-server-express": "^2.9.16",
"babel-plugin-transform-runtime": "^6.23.0", "babel-plugin-transform-runtime": "^6.23.0",
"bcryptjs": "~2.4.3", "bcryptjs": "~2.4.3",
"cheerio": "~1.0.0-rc.3", "cheerio": "~1.0.0-rc.3",
"cors": "~2.8.5", "cors": "~2.8.5",
"cross-env": "~6.0.3", "cross-env": "~6.0.3",
"date-fns": "2.8.1", "date-fns": "2.9.0",
"debug": "~4.1.1", "debug": "~4.1.1",
"dotenv": "~8.2.0", "dotenv": "~8.2.0",
"express": "^4.17.1", "express": "^4.17.1",
@ -62,26 +62,26 @@
"linkifyjs": "~2.1.8", "linkifyjs": "~2.1.8",
"lodash": "~4.17.14", "lodash": "~4.17.14",
"merge-graphql-schemas": "^1.7.6", "merge-graphql-schemas": "^1.7.6",
"metascraper": "^5.9.4", "metascraper": "^5.9.5",
"metascraper-audio": "^5.8.13", "metascraper-audio": "^5.9.5",
"metascraper-author": "^5.8.13", "metascraper-author": "^5.9.5",
"metascraper-clearbit-logo": "^5.3.0", "metascraper-clearbit-logo": "^5.3.0",
"metascraper-date": "^5.8.13", "metascraper-date": "^5.9.5",
"metascraper-description": "^5.8.15", "metascraper-description": "^5.9.5",
"metascraper-image": "^5.9.4", "metascraper-image": "^5.9.5",
"metascraper-lang": "^5.8.13", "metascraper-lang": "^5.9.5",
"metascraper-lang-detector": "^4.10.2", "metascraper-lang-detector": "^4.10.2",
"metascraper-logo": "^5.8.13", "metascraper-logo": "^5.9.5",
"metascraper-publisher": "^5.8.13", "metascraper-publisher": "^5.9.5",
"metascraper-soundcloud": "^5.9.0", "metascraper-soundcloud": "^5.9.5",
"metascraper-title": "^5.8.13", "metascraper-title": "^5.9.5",
"metascraper-url": "^5.8.13", "metascraper-url": "^5.9.5",
"metascraper-video": "^5.8.13", "metascraper-video": "^5.9.5",
"metascraper-youtube": "^5.8.13", "metascraper-youtube": "^5.9.5",
"minimatch": "^3.0.4", "minimatch": "^3.0.4",
"mustache": "^3.2.1", "mustache": "^3.2.1",
"neo4j-driver": "^4.0.1", "neo4j-driver": "^4.0.1",
"neo4j-graphql-js": "^2.11.3", "neo4j-graphql-js": "^2.11.5",
"neode": "^0.3.7", "neode": "^0.3.7",
"node-fetch": "~2.6.0", "node-fetch": "~2.6.0",
"nodemailer": "^6.4.2", "nodemailer": "^6.4.2",
@ -97,13 +97,13 @@
"xregexp": "^4.2.4" "xregexp": "^4.2.4"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "~7.7.7", "@babel/cli": "~7.8.0",
"@babel/core": "~7.7.7", "@babel/core": "~7.7.7",
"@babel/node": "~7.7.7", "@babel/node": "~7.8.0",
"@babel/plugin-proposal-throw-expressions": "^7.7.4", "@babel/plugin-proposal-throw-expressions": "^7.8.0",
"@babel/preset-env": "~7.7.7", "@babel/preset-env": "~7.8.2",
"@babel/register": "~7.7.0", "@babel/register": "~7.8.0",
"apollo-server-testing": "~2.9.15", "apollo-server-testing": "~2.9.16",
"babel-core": "~7.0.0-0", "babel-core": "~7.0.0-0",
"babel-eslint": "~10.0.3", "babel-eslint": "~10.0.3",
"babel-jest": "~24.9.0", "babel-jest": "~24.9.0",
@ -112,8 +112,8 @@
"eslint": "~6.8.0", "eslint": "~6.8.0",
"eslint-config-prettier": "~6.9.0", "eslint-config-prettier": "~6.9.0",
"eslint-config-standard": "~14.1.0", "eslint-config-standard": "~14.1.0",
"eslint-plugin-import": "~2.19.1", "eslint-plugin-import": "~2.20.0",
"eslint-plugin-jest": "~23.3.0", "eslint-plugin-jest": "~23.6.0",
"eslint-plugin-node": "~11.0.0", "eslint-plugin-node": "~11.0.0",
"eslint-plugin-prettier": "~3.1.2", "eslint-plugin-prettier": "~3.1.2",
"eslint-plugin-promise": "~4.2.1", "eslint-plugin-promise": "~4.2.1",

View File

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

View File

@ -0,0 +1,23 @@
import { getMutedUsers } from '../users.js'
import { mergeWith, isArray } from 'lodash'
export const filterForMutedUsers = async (params, context) => {
if (!context.user) return params
const [mutedUsers] = await Promise.all([getMutedUsers(context)])
const mutedUsersIds = [...mutedUsers.map(user => user.id)]
if (!mutedUsersIds.length) return params
params.filter = mergeWith(
params.filter,
{
author_not: { id_in: mutedUsersIds },
},
(objValue, srcValue) => {
if (isArray(objValue)) {
return objValue.concat(srcValue)
}
},
)
return params
}

View File

@ -1,30 +1,10 @@
import uuid from 'uuid/v4' import uuid from 'uuid/v4'
import { neo4jgraphql } from 'neo4j-graphql-js' import { neo4jgraphql } from 'neo4j-graphql-js'
import fileUpload from './fileUpload' import { isEmpty } from 'lodash'
import { getMutedUsers } from './users.js'
import { mergeWith, isArray, isEmpty } from 'lodash'
import { UserInputError } from 'apollo-server' import { UserInputError } from 'apollo-server'
import fileUpload from './fileUpload'
import Resolver from './helpers/Resolver' import Resolver from './helpers/Resolver'
import { filterForMutedUsers } from './helpers/filterForMutedUsers'
const filterForMutedUsers = async (params, context) => {
if (!context.user) return params
const [mutedUsers] = await Promise.all([getMutedUsers(context)])
const mutedUsersIds = [...mutedUsers.map(user => user.id)]
if (!mutedUsersIds.length) return params
params.filter = mergeWith(
params.filter,
{
author_not: { id_in: mutedUsersIds },
},
(objValue, srcValue) => {
if (isArray(objValue)) {
return objValue.concat(srcValue)
}
},
)
return params
}
const maintainPinnedPosts = params => { const maintainPinnedPosts = params => {
const pinnedPostFilter = { pinned: true } const pinnedPostFilter = { pinned: true }

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

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

View File

@ -230,4 +230,18 @@ type Query {
PostsEmotionsCountByEmotion(postId: ID!, data: _EMOTEDInput!): Int! PostsEmotionsCountByEmotion(postId: ID!, data: _EMOTEDInput!): Int!
PostsEmotionsByCurrentUser(postId: ID!): [String] PostsEmotionsByCurrentUser(postId: ID!): [String]
profilePagePosts(filter: _PostFilter, first: Int, offset: Int, orderBy: [_PostOrdering]): [Post] profilePagePosts(filter: _PostFilter, first: Int, offset: Int, orderBy: [_PostOrdering]): [Post]
findPosts(query: String!, limit: Int = 10, filter: _PostFilter): [Post]!
@cypher(
statement: """
CALL db.index.fulltext.queryNodes('post_fulltext_search', $query)
YIELD node as post, score
MATCH (post)<-[:WROTE]-(user:User)
WHERE score >= 0.2
AND NOT user.deleted = true AND NOT user.disabled = true
AND NOT post.deleted = true AND NOT post.disabled = true
AND NOT user.id in COALESCE($filter.author_not.id_in, [])
RETURN post
LIMIT $limit
"""
)
} }

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "human-connection", "name": "human-connection",
"version": "0.2.0", "version": "0.2.1",
"description": "Fullstack and API tests with cypress and cucumber for Human Connection", "description": "Fullstack and API tests with cypress and cucumber for Human Connection",
"author": "Human Connection gGmbh", "author": "Human Connection gGmbh",
"license": "MIT", "license": "MIT",
@ -21,19 +21,19 @@
"version": "auto-changelog -p" "version": "auto-changelog -p"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.7.7", "@babel/core": "^7.8.0",
"@babel/preset-env": "^7.7.7", "@babel/preset-env": "^7.8.2",
"@babel/register": "^7.7.7", "@babel/register": "^7.8.0",
"auto-changelog": "^1.16.2", "auto-changelog": "^1.16.2",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"codecov": "^3.6.1", "codecov": "^3.6.1",
"cross-env": "^6.0.3", "cross-env": "^6.0.3",
"cucumber": "^6.0.5", "cucumber": "^6.0.5",
"cypress": "^3.8.1", "cypress": "^3.8.2",
"cypress-cucumber-preprocessor": "^1.19.0", "cypress-cucumber-preprocessor": "^1.19.0",
"cypress-file-upload": "^3.5.3", "cypress-file-upload": "^3.5.3",
"cypress-plugin-retries": "^1.5.2", "cypress-plugin-retries": "^1.5.2",
"date-fns": "^2.8.1", "date-fns": "^2.9.0",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"expect": "^24.9.0", "expect": "^24.9.0",
"faker": "Marak/faker.js#master", "faker": "Marak/faker.js#master",

View File

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

View File

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

View File

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

View File

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

View File

@ -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

@ -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>

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 @@
<ds-flex-item <ds-flex-item
:width="{ base: '85%', sm: '85%', md: '50%', lg: '50%' }" :width="{ base: '85%', sm: '85%', md: '50%', lg: '50%' }"
:class="{ 'hide-mobile-menu': !toggleMobileMenu }" :class="{ 'hide-mobile-menu': !toggleMobileMenu }"
id="nav-search-box"
v-if="isLoggedIn"
> >
<div id="nav-search-box" v-if="isLoggedIn"> <search-field />
<search-input
id="nav-search"
:delay="300"
:pending="quickSearchPending"
:results="quickSearchResults"
@clear="quickSearchClear"
@search="value => quickSearch({ value })"
@select="goToPost"
/>
</div>
</ds-flex-item> </ds-flex-item>
<ds-flex-item <ds-flex-item
v-if="isLoggedIn" v-if="isLoggedIn"
@ -90,9 +82,9 @@
</template> </template>
<script> <script>
import { mapGetters, mapActions } from 'vuex' import { mapGetters } from 'vuex'
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch' 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 Modal from '~/components/Modal'
import NotificationMenu from '~/components/NotificationMenu/NotificationMenu' import NotificationMenu from '~/components/NotificationMenu/NotificationMenu'
import seo from '~/mixins/seo' import seo from '~/mixins/seo'
@ -104,7 +96,7 @@ import AvatarMenu from '~/components/AvatarMenu/AvatarMenu'
export default { export default {
components: { components: {
LocaleSwitch, LocaleSwitch,
SearchInput, SearchField,
Modal, Modal,
NotificationMenu, NotificationMenu,
AvatarMenu, AvatarMenu,
@ -122,8 +114,6 @@ export default {
computed: { computed: {
...mapGetters({ ...mapGetters({
isLoggedIn: 'auth/isLoggedIn', isLoggedIn: 'auth/isLoggedIn',
quickSearchResults: 'search/quickResults',
quickSearchPending: 'search/quickPending',
}), }),
showFilterPostsDropdown() { showFilterPostsDropdown() {
const [firstRoute] = this.$route.matched const [firstRoute] = this.$route.matched
@ -136,18 +126,6 @@ export default {
}, },
}, },
methods: { 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() { toggleMobileMenuView() {
this.toggleMobileMenu = !this.toggleMobileMenu this.toggleMobileMenu = !this.toggleMobileMenu
}, },

View File

@ -451,7 +451,7 @@
"filterFollow": "Beiträge filtern von Usern denen ich folge", "filterFollow": "Beiträge filtern von Usern denen ich folge",
"filterALL": "Alle Beiträge anzeigen", "filterALL": "Alle Beiträge anzeigen",
"success": "Gespeichert!", "success": "Gespeichert!",
"languageSelectLabel": "Sprache", "languageSelectLabel": "Sprache deines Beitrags",
"languageSelectText": "Sprache wählen", "languageSelectText": "Sprache wählen",
"categories": { "categories": {
"infoSelectedNoOfMaxCategories": "{chosen} von {max} Kategorien ausgewählt" "infoSelectedNoOfMaxCategories": "{chosen} von {max} Kategorien ausgewählt"
@ -515,7 +515,11 @@
"search": { "search": {
"placeholder": "Suchen", "placeholder": "Suchen",
"hint": "Wonach suchst Du?", "hint": "Wonach suchst Du?",
"failed": "Nichts gefunden" "failed": "Nichts gefunden",
"heading": {
"Post": "Beiträge",
"User": "Benutzer"
}
}, },
"components": { "components": {
"password-reset": { "password-reset": {
@ -548,7 +552,7 @@
"unavailable": "Leider ist die öffentliche Registrierung von Benutzerkonten auf diesem Server derzeit nicht möglich.", "unavailable": "Leider ist die öffentliche Registrierung von Benutzerkonten auf diesem Server derzeit nicht möglich.",
"title": "Mach mit bei Human Connection!", "title": "Mach mit bei Human Connection!",
"form": { "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.", "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", "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.", "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.", "unavailable": "Unfortunately, public registration of user accounts is not available right now on this server.",
"title": "Join Human Connection!", "title": "Join Human Connection!",
"form": { "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>.", "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> ", "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.", "minimum-age": "I'm 18 years or older.",
@ -199,7 +199,11 @@
"search": { "search": {
"placeholder": "Search", "placeholder": "Search",
"hint": "What are you searching for?", "hint": "What are you searching for?",
"failed": "Nothing found" "failed": "Nothing found",
"heading": {
"Post": "Posts",
"User": "Users"
}
}, },
"settings": { "settings": {
"name": "Settings", "name": "Settings",

View File

@ -59,7 +59,7 @@
"dependencies": { "dependencies": {
"@human-connection/styleguide": "0.5.22", "@human-connection/styleguide": "0.5.22",
"@nuxtjs/apollo": "^4.0.0-rc19", "@nuxtjs/apollo": "^4.0.0-rc19",
"@nuxtjs/axios": "~5.8.0", "@nuxtjs/axios": "~5.9.3",
"@nuxtjs/dotenv": "~1.4.1", "@nuxtjs/dotenv": "~1.4.1",
"@nuxtjs/pwa": "^3.0.0-beta.19", "@nuxtjs/pwa": "^3.0.0-beta.19",
"@nuxtjs/sentry": "^3.0.1", "@nuxtjs/sentry": "^3.0.1",
@ -70,13 +70,13 @@
"cookie-universal-nuxt": "~2.1.0", "cookie-universal-nuxt": "~2.1.0",
"cropperjs": "^1.5.5", "cropperjs": "^1.5.5",
"cross-env": "~6.0.3", "cross-env": "~6.0.3",
"date-fns": "2.8.1", "date-fns": "2.9.0",
"express": "~4.17.1", "express": "~4.17.1",
"graphql": "~14.5.8", "graphql": "~14.5.8",
"jsonwebtoken": "~8.5.1", "jsonwebtoken": "~8.5.1",
"linkify-it": "~2.2.0", "linkify-it": "~2.2.0",
"node-fetch": "^2.6.0", "node-fetch": "^2.6.0",
"nuxt": "~2.10.2", "nuxt": "~2.11.0",
"nuxt-dropzone": "^1.0.4", "nuxt-dropzone": "^1.0.4",
"nuxt-env": "~0.1.0", "nuxt-env": "~0.1.0",
"stack-utils": "^2.0.1", "stack-utils": "^2.0.1",
@ -97,12 +97,12 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "~7.7.7", "@babel/core": "~7.7.7",
"@babel/plugin-syntax-dynamic-import": "^7.2.0", "@babel/plugin-syntax-dynamic-import": "^7.8.0",
"@babel/preset-env": "~7.7.7", "@babel/preset-env": "~7.7.7",
"@storybook/addon-a11y": "^5.2.8", "@storybook/addon-a11y": "^5.2.8",
"@storybook/addon-actions": "^5.2.8", "@storybook/addon-actions": "^5.3.2",
"@storybook/addon-notes": "^5.2.8", "@storybook/addon-notes": "^5.3.1",
"@storybook/vue": "~5.2.8", "@storybook/vue": "~5.3.1",
"@vue/cli-shared-utils": "~4.1.2", "@vue/cli-shared-utils": "~4.1.2",
"@vue/eslint-config-prettier": "~6.0.0", "@vue/eslint-config-prettier": "~6.0.0",
"@vue/server-test-utils": "~1.0.0-beta.30", "@vue/server-test-utils": "~1.0.0-beta.30",
@ -115,7 +115,7 @@
"babel-plugin-require-context-hook": "^1.0.0", "babel-plugin-require-context-hook": "^1.0.0",
"babel-preset-vue": "~2.0.2", "babel-preset-vue": "~2.0.2",
"core-js": "~2.6.10", "core-js": "~2.6.10",
"css-loader": "~3.4.1", "css-loader": "~3.4.2",
"eslint": "~6.8.0", "eslint": "~6.8.0",
"eslint-config-prettier": "~6.9.0", "eslint-config-prettier": "~6.9.0",
"eslint-config-standard": "~14.1.0", "eslint-config-standard": "~14.1.0",
@ -143,6 +143,6 @@
"vue-jest": "~3.0.5", "vue-jest": "~3.0.5",
"vue-loader": "~15.8.3", "vue-loader": "~15.8.3",
"vue-svg-loader": "~0.15.0", "vue-svg-loader": "~0.15.0",
"vue-template-compiler": "^2.6.10" "vue-template-compiler": "^2.6.11"
} }
} }

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

1110
yarn.lock

File diff suppressed because it is too large Load Diff