Merge branch 'master' into 1680-Direct_answer_on_Comment

This commit is contained in:
Alexander Friedland 2020-01-22 11:46:04 +01:00 committed by GitHub
commit 9776efe760
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
135 changed files with 4572 additions and 2396 deletions

View File

@ -6,7 +6,6 @@ addons:
- libgconf-2-4
snaps:
- docker
- chromium
install:
- yarn global add wait-on

7
.versionrc.json Normal file
View File

@ -0,0 +1,7 @@
{
"bumpFiles": [
"package.json",
"backend/package.json",
"webapp/package.json"
]
}

View File

@ -4,6 +4,106 @@ 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.2](https://github.com/Human-Connection/Human-Connection/compare/v0.2.1...v0.2.2)
> 20 January 2020
- build(deps): bump metascraper-title from 5.10.3 to 5.10.5 in /backend [`#2835`](https://github.com/Human-Connection/Human-Connection/pull/2835)
- build(deps): bump metascraper-publisher in /backend [`#2836`](https://github.com/Human-Connection/Human-Connection/pull/2836)
- build(deps): bump metascraper-audio from 5.10.3 to 5.10.5 in /backend [`#2840`](https://github.com/Human-Connection/Human-Connection/pull/2840)
- build(deps): bump metascraper-author from 5.10.3 to 5.10.5 in /backend [`#2838`](https://github.com/Human-Connection/Human-Connection/pull/2838)
- build(deps): bump metascraper-url from 5.10.3 to 5.10.5 in /backend [`#2832`](https://github.com/Human-Connection/Human-Connection/pull/2832)
- build(deps): bump metascraper-lang from 5.10.3 to 5.10.5 in /backend [`#2831`](https://github.com/Human-Connection/Human-Connection/pull/2831)
- refactor(modules): Various import fixes [`#2802`](https://github.com/Human-Connection/Human-Connection/pull/2802)
- build(deps): bump metascraper-description from 5.10.3 to 5.10.5 in /backend [`#2839`](https://github.com/Human-Connection/Human-Connection/pull/2839)
- build(deps-dev): bump @storybook/addon-notes from 5.3.5 to 5.3.6 in /webapp [`#2834`](https://github.com/Human-Connection/Human-Connection/pull/2834)
- build(deps): bump metascraper-youtube from 5.10.3 to 5.10.5 in /backend [`#2833`](https://github.com/Human-Connection/Human-Connection/pull/2833)
- build(deps): bump metascraper from 5.10.3 to 5.10.5 in /backend [`#2830`](https://github.com/Human-Connection/Human-Connection/pull/2830)
- build(deps): bump metascraper-soundcloud from 5.10.3 to 5.10.5 in /backend [`#2829`](https://github.com/Human-Connection/Human-Connection/pull/2829)
- fix(translations): Remove duplicate and mistranslated item from code of conduct [`#2725`](https://github.com/Human-Connection/Human-Connection/pull/2725)
- build(deps-dev): bump @storybook/addon-a11y from 5.3.3 to 5.3.6 in /webapp [`#2820`](https://github.com/Human-Connection/Human-Connection/pull/2820)
- build(deps): bump metascraper from 5.10.2 to 5.10.3 in /backend [`#2808`](https://github.com/Human-Connection/Human-Connection/pull/2808)
- build(deps-dev): bump @storybook/vue from 5.3.3 to 5.3.6 in /webapp [`#2819`](https://github.com/Human-Connection/Human-Connection/pull/2819)
- build(deps): bump faker from `9fd8d7d` to `3b2fa4a` in /backend [`#2803`](https://github.com/Human-Connection/Human-Connection/pull/2803)
- build(deps-dev): bump faker from `9fd8d7d` to `3b2fa4a` [`#2804`](https://github.com/Human-Connection/Human-Connection/pull/2804)
- build(deps-dev): bump @storybook/addon-a11y in /webapp [`#2809`](https://github.com/Human-Connection/Human-Connection/pull/2809)
- build(deps): bump uuid from 3.3.3 to 3.4.0 in /backend [`#2810`](https://github.com/Human-Connection/Human-Connection/pull/2810)
- build(deps): bump metascraper-image from 5.9.5 to 5.10.3 in /backend [`#2811`](https://github.com/Human-Connection/Human-Connection/pull/2811)
- build(deps-dev): bump node-sass from 4.13.0 to 4.13.1 in /webapp [`#2812`](https://github.com/Human-Connection/Human-Connection/pull/2812)
- build(deps): bump metascraper-audio from 5.9.5 to 5.10.3 in /backend [`#2813`](https://github.com/Human-Connection/Human-Connection/pull/2813)
- build(deps): bump metascraper-soundcloud in /backend [`#2815`](https://github.com/Human-Connection/Human-Connection/pull/2815)
- build(deps-dev): bump @storybook/addon-notes in /webapp [`#2816`](https://github.com/Human-Connection/Human-Connection/pull/2816)
- build(deps-dev): bump @storybook/addon-actions from 5.3.3 to 5.3.5 in /webapp [`#2807`](https://github.com/Human-Connection/Human-Connection/pull/2807)
- build(deps): bump metascraper-description from 5.9.5 to 5.10.3 in /backend [`#2806`](https://github.com/Human-Connection/Human-Connection/pull/2806)
- build(deps): bump mustache from 3.2.1 to 4.0.0 in /backend [`#2805`](https://github.com/Human-Connection/Human-Connection/pull/2805)
- 🍰 feat(webapp): Display deployed version in footer [`#2728`](https://github.com/Human-Connection/Human-Connection/pull/2728)
- fix: cypress breaks locally in login step [`#2776`](https://github.com/Human-Connection/Human-Connection/pull/2776)
- build(deps-dev): bump @vue/test-utils from 1.0.0-beta.29 to 1.0.0-beta.30 in /webapp [`#2378`](https://github.com/Human-Connection/Human-Connection/pull/2378)
- build(deps): bump metascraper-youtube from 5.9.5 to 5.10.3 in /backend [`#2794`](https://github.com/Human-Connection/Human-Connection/pull/2794)
- build(deps): bump metascraper-video from 5.9.5 to 5.10.3 in /backend [`#2795`](https://github.com/Human-Connection/Human-Connection/pull/2795)
- build(deps): bump metascraper-logo from 5.9.5 to 5.10.3 in /backend [`#2796`](https://github.com/Human-Connection/Human-Connection/pull/2796)
- refactor(styleguide): improve emotion buttons and header responsiveness [`#2582`](https://github.com/Human-Connection/Human-Connection/pull/2582)
- build(deps): bump metascraper-url from 5.9.5 to 5.10.3 in /backend [`#2793`](https://github.com/Human-Connection/Human-Connection/pull/2793)
- build(deps): bump metascraper-author from 5.9.5 to 5.10.3 in /backend [`#2789`](https://github.com/Human-Connection/Human-Connection/pull/2789)
- build(deps): bump metascraper-lang from 5.9.5 to 5.10.3 in /backend [`#2790`](https://github.com/Human-Connection/Human-Connection/pull/2790)
- build(deps): bump metascraper-publisher from 5.9.5 to 5.10.3 in /backend [`#2792`](https://github.com/Human-Connection/Human-Connection/pull/2792)
- build(deps): bump metascraper-title from 5.9.5 to 5.10.3 in /backend [`#2791`](https://github.com/Human-Connection/Human-Connection/pull/2791)
- build(deps): bump @sentry/node from 5.11.0 to 5.11.1 in /backend [`#2788`](https://github.com/Human-Connection/Human-Connection/pull/2788)
- build(deps): bump metascraper-date from 5.9.5 to 5.10.3 in /backend [`#2787`](https://github.com/Human-Connection/Human-Connection/pull/2787)
- build(deps-dev): bump @babel/node from 7.8.0 to 7.8.3 in /backend [`#2754`](https://github.com/Human-Connection/Human-Connection/pull/2754)
- refactor(styleguide): migrate and redesign buttons [`#2562`](https://github.com/Human-Connection/Human-Connection/pull/2562)
- build(deps-dev): bump @babel/core from 7.8.0 to 7.8.3 in /backend [`#2760`](https://github.com/Human-Connection/Human-Connection/pull/2760)
- build(deps-dev): bump @storybook/addon-actions from 5.3.2 to 5.3.3 in /webapp [`#2782`](https://github.com/Human-Connection/Human-Connection/pull/2782)
- build(deps-dev): bump sass-loader from 8.0.0 to 8.0.2 in /webapp [`#2781`](https://github.com/Human-Connection/Human-Connection/pull/2781)
- build(deps-dev): bump @babel/plugin-syntax-dynamic-import from 7.8.0 to 7.8.3 in /webapp [`#2780`](https://github.com/Human-Connection/Human-Connection/pull/2780)
- build(deps-dev): bump @storybook/addon-a11y from 5.3.2 to 5.3.3 in /webapp [`#2779`](https://github.com/Human-Connection/Human-Connection/pull/2779)
- build(deps): bump metascraper from 5.9.5 to 5.10.2 in /backend [`#2778`](https://github.com/Human-Connection/Human-Connection/pull/2778)
- build(deps-dev): bump @babel/preset-env from 7.7.7 to 7.8.3 in /webapp [`#2767`](https://github.com/Human-Connection/Human-Connection/pull/2767)
- build(deps-dev): bump @babel/plugin-proposal-throw-expressions from 7.8.0 to 7.8.3 in /backend [`#2757`](https://github.com/Human-Connection/Human-Connection/pull/2757)
- build(deps-dev): bump @storybook/vue from 5.3.1 to 5.3.3 in /webapp [`#2772`](https://github.com/Human-Connection/Human-Connection/pull/2772)
- build(deps-dev): bump @babel/preset-env from 7.8.2 to 7.8.3 [`#2758`](https://github.com/Human-Connection/Human-Connection/pull/2758)
- build(deps-dev): bump eslint-plugin-import from 2.19.1 to 2.20.0 in /webapp [`#2748`](https://github.com/Human-Connection/Human-Connection/pull/2748)
- build(deps-dev): bump @storybook/addon-notes from 5.3.1 to 5.3.3 in /webapp [`#2771`](https://github.com/Human-Connection/Human-Connection/pull/2771)
- build(deps-dev): bump @babel/core from 7.7.7 to 7.8.3 in /webapp [`#2769`](https://github.com/Human-Connection/Human-Connection/pull/2769)
- build(deps-dev): bump @babel/register from 7.8.0 to 7.8.3 [`#2764`](https://github.com/Human-Connection/Human-Connection/pull/2764)
- build(deps-dev): bump @babel/preset-env from 7.8.2 to 7.8.3 in /backend [`#2755`](https://github.com/Human-Connection/Human-Connection/pull/2755)
- build(deps-dev): bump eslint-plugin-jest from 23.3.0 to 23.6.0 in /webapp [`#2768`](https://github.com/Human-Connection/Human-Connection/pull/2768)
- build(deps-dev): bump @babel/cli from 7.8.0 to 7.8.3 in /backend [`#2763`](https://github.com/Human-Connection/Human-Connection/pull/2763)
- build(deps-dev): bump cypress-cucumber-preprocessor from 1.19.0 to 2.0.1 [`#2761`](https://github.com/Human-Connection/Human-Connection/pull/2761)
- build(deps-dev): bump @storybook/addon-a11y from 5.2.8 to 5.3.2 in /webapp [`#2759`](https://github.com/Human-Connection/Human-Connection/pull/2759)
- build(deps-dev): bump @babel/core from 7.8.0 to 7.8.3 [`#2756`](https://github.com/Human-Connection/Human-Connection/pull/2756)
- build(deps-dev): bump @babel/register from 7.8.0 to 7.8.3 in /backend [`#2753`](https://github.com/Human-Connection/Human-Connection/pull/2753)
- build(deps): [security] bump serialize-javascript from 2.1.0 to 2.1.2 in /webapp [`#2752`](https://github.com/Human-Connection/Human-Connection/pull/2752)
- build(deps-dev): bump @babel/core from 7.7.7 to 7.8.0 in /backend [`#2743`](https://github.com/Human-Connection/Human-Connection/pull/2743)
- build(deps-dev): bump @storybook/addon-actions from 5.2.8 to 5.3.2 in /webapp [`#2751`](https://github.com/Human-Connection/Human-Connection/pull/2751)
- build(deps-dev): bump @babel/register from 7.7.7 to 7.8.0 in /backend [`#2735`](https://github.com/Human-Connection/Human-Connection/pull/2735)
- build(deps-dev): bump @babel/plugin-syntax-dynamic-import from 7.7.4 to 7.8.0 in /webapp [`#2746`](https://github.com/Human-Connection/Human-Connection/pull/2746)
- build(deps-dev): bump @babel/preset-env from 7.7.7 to 7.8.2 in /backend [`#2739`](https://github.com/Human-Connection/Human-Connection/pull/2739)
- build(deps-dev): bump @babel/cli from 7.7.7 to 7.8.0 in /backend [`#2744`](https://github.com/Human-Connection/Human-Connection/pull/2744)
- Issues marked as bounty never become stale [`#2726`](https://github.com/Human-Connection/Human-Connection/pull/2726)
- build(deps-dev): bump css-loader from 3.4.1 to 3.4.2 in /webapp [`#2747`](https://github.com/Human-Connection/Human-Connection/pull/2747)
- build(deps-dev): bump @storybook/addon-notes from 5.2.8 to 5.3.1 in /webapp [`#2742`](https://github.com/Human-Connection/Human-Connection/pull/2742)
- build(deps-dev): bump @babel/plugin-proposal-throw-expressions from 7.7.4 to 7.8.0 in /backend [`#2741`](https://github.com/Human-Connection/Human-Connection/pull/2741)
- build(deps-dev): bump eslint-plugin-import from 2.19.1 to 2.20.0 in /backend [`#2737`](https://github.com/Human-Connection/Human-Connection/pull/2737)
- build(deps-dev): bump @babel/preset-env from 7.7.7 to 7.8.2 [`#2732`](https://github.com/Human-Connection/Human-Connection/pull/2732)
- build(deps): bump @nuxtjs/axios from 5.9.2 to 5.9.3 in /webapp [`#2740`](https://github.com/Human-Connection/Human-Connection/pull/2740)
- build(deps-dev): bump @storybook/vue from 5.2.8 to 5.3.1 in /webapp [`#2738`](https://github.com/Human-Connection/Human-Connection/pull/2738)
- build(deps-dev): bump cypress from 3.8.1 to 3.8.2 [`#2734`](https://github.com/Human-Connection/Human-Connection/pull/2734)
- build(deps-dev): bump @babel/node from 7.7.7 to 7.8.0 in /backend [`#2733`](https://github.com/Human-Connection/Human-Connection/pull/2733)
- build(deps-dev): bump eslint-plugin-jest from 23.3.0 to 23.6.0 in /backend [`#2731`](https://github.com/Human-Connection/Human-Connection/pull/2731)
- build(deps-dev): bump @babel/core from 7.7.7 to 7.8.0 [`#2730`](https://github.com/Human-Connection/Human-Connection/pull/2730)
- build(deps-dev): bump @babel/register from 7.7.7 to 7.8.0 [`#2729`](https://github.com/Human-Connection/Human-Connection/pull/2729)
- build(deps): bump nuxt from 2.10.2 to 2.11.0 in /webapp [`#2552`](https://github.com/Human-Connection/Human-Connection/pull/2552)
- Update yarn.lock after dependabot update [`#2724`](https://github.com/Human-Connection/Human-Connection/pull/2724)
- build(deps): bump @nuxtjs/axios from 5.8.0 to 5.9.2 in /webapp [`#2657`](https://github.com/Human-Connection/Human-Connection/pull/2657)
- Update to version 0.2.1 [`#2722`](https://github.com/Human-Connection/Human-Connection/pull/2722)
- refactor(modules): Various import fixes [`#2773`](https://github.com/Human-Connection/Human-Connection/issues/2773) [`#2774`](https://github.com/Human-Connection/Human-Connection/issues/2774)
- feat(webapp): Display deployed version in footer [`#1831`](https://github.com/Human-Connection/Human-Connection/issues/1831)
- fix #2229 [`#2229`](https://github.com/Human-Connection/Human-Connection/issues/2229)
- build(deps-dev): bump @storybook/addon-actions in /webapp [`d0124bf`](https://github.com/Human-Connection/Human-Connection/commit/d0124bf2b4b4a641c9af76d6d2f7b5aa075ade90)
- refactor and use base-button in SearchableInput [`fcbe612`](https://github.com/Human-Connection/Human-Connection/commit/fcbe6125f35c0dd23e2ba1ae63f539f5ef5990ea)
- Update `vue-test-utils` and follow updated docs [`8c29ad9`](https://github.com/Human-Connection/Human-Connection/commit/8c29ad947b72fbaa173d070221cdf35b7ab6aaa5)
#### [v0.2.1](https://github.com/Human-Connection/Human-Connection/compare/v0.2.0...v0.2.1)
> 10 January 2020
@ -107,16 +207,16 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- 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)
- manage button states and color schemes with mixin [`1b9249c`](https://github.com/Human-Connection/Human-Connection/commit/1b9249c685e34eb2e94b31ee0ec22421c6aa6a73)
- 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
- Update to version 0.2.0 [`#2584`](https://github.com/Human-Connection/Human-Connection/pull/2584)
- 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)
@ -203,7 +303,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- 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)
#### [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.11...v0.1.12)
> 10 December 2019
@ -313,6 +413,16 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- 2329 normalize emails in login form [`#2330`](https://github.com/Human-Connection/Human-Connection/pull/2330)
- Lokalise: Translations update [`#2327`](https://github.com/Human-Connection/Human-Connection/pull/2327)
- Changed translation must change test :( [`#2310`](https://github.com/Human-Connection/Human-Connection/pull/2310)
- Merge pull request #2443 from Human-Connection/2237-longer-comments [`#2237`](https://github.com/Human-Connection/Human-Connection/issues/2237)
- fix #2329: Normalize email on login in the backend [`#2329`](https://github.com/Human-Connection/Human-Connection/issues/2329)
- Lokalise: update of webapp/locales/ru.json [`3e52ee0`](https://github.com/Human-Connection/Human-Connection/commit/3e52ee090c88c357b796895370d126f8bb5529f0)
- Lokalise: update of webapp/locales/de.json [`d2b3396`](https://github.com/Human-Connection/Human-Connection/commit/d2b3396e9b44bac0e767ee970e083d1847426b26)
- Lokalise: update of webapp/locales/pt.json [`bcd9f0e`](https://github.com/Human-Connection/Human-Connection/commit/bcd9f0ec93cfab2661589d72a3b3f38455ec4d51)
#### [v0.1.11](https://github.com/Human-Connection/Human-Connection/compare/v0.1.10...v0.1.11)
> 22 November 2019
- build(deps-dev): bump apollo-server-testing from 2.9.9 to 2.9.12 in /backend [`#2318`](https://github.com/Human-Connection/Human-Connection/pull/2318)
- build(deps-dev): bump fuse.js from 3.4.5 to 3.4.6 in /webapp [`#2314`](https://github.com/Human-Connection/Human-Connection/pull/2314)
- build(deps-dev): bump eslint-config-prettier from 6.6.0 to 6.7.0 in /webapp [`#2302`](https://github.com/Human-Connection/Human-Connection/pull/2302)
@ -362,13 +472,11 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- build(deps-dev): bump eslint from 5.16.0 to 6.6.0 in /webapp [`#2205`](https://github.com/Human-Connection/Human-Connection/pull/2205)
- Add locale to undefined to null [`#2233`](https://github.com/Human-Connection/Human-Connection/pull/2233)
- Update to version 0.1.10 [`#2231`](https://github.com/Human-Connection/Human-Connection/pull/2231)
- Merge pull request #2443 from Human-Connection/2237-longer-comments [`#2237`](https://github.com/Human-Connection/Human-Connection/issues/2237)
- 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)
- 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)
- 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)
- set up global localVue [`77f4810`](https://github.com/Human-Connection/Human-Connection/commit/77f4810ddc963386bc68d3e8a5e078ef4cf270b2)
#### [v0.1.10](https://github.com/Human-Connection/Human-Connection/compare/v0.1.9...v0.1.10)
@ -428,8 +536,8 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Update feature template [`#2116`](https://github.com/Human-Connection/Human-Connection/pull/2116)
- Update to version 0.1.9 [`#2114`](https://github.com/Human-Connection/Human-Connection/pull/2114)
- remove package-lock.json [`3cf3c31`](https://github.com/Human-Connection/Human-Connection/commit/3cf3c31808dc6ae59fb9c6ec33e9e178c5556438)
- Extract AvatarMenu into its own component [`994a0b0`](https://github.com/Human-Connection/Human-Connection/commit/994a0b049d1803784d9c06383872f1c9e33095a0)
- Add notifications page with Notifications in table [`7cdc12f`](https://github.com/Human-Connection/Human-Connection/commit/7cdc12f4b9943062e15a874dd39f8a50142b6c61)
- add current file [`26c0d4d`](https://github.com/Human-Connection/Human-Connection/commit/26c0d4d83e4418a2378e05b66b6b47461f82735f)
- Finish portuguese translations [`15c671c`](https://github.com/Human-Connection/Human-Connection/commit/15c671c4a8aae86317896ca30601389504bce9e1)
#### [v0.1.9](https://github.com/Human-Connection/Human-Connection/compare/v0.1.8...v0.1.9)
@ -497,9 +605,9 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- first implementation [`aeae72f`](https://github.com/Human-Connection/Human-Connection/commit/aeae72f6918861aa2a4c64d0b32c847d9e857e93)
- build(deps-dev): bump eslint-plugin-jest in /backend [`6c1bd53`](https://github.com/Human-Connection/Human-Connection/commit/6c1bd535ac482eb0a05d21e227a476800717a19e)
#### [v0.1.8](https://github.com/Human-Connection/Human-Connection/compare/0.1.7...v0.1.8)
#### [v0.1.8](https://github.com/Human-Connection/Human-Connection/compare/v0.1.7...v0.1.8)
> 25 October 2019
> 24 October 2019
- add FAQ _blank-href in Footer [`#2028`](https://github.com/Human-Connection/Human-Connection/pull/2028)
- fix: Don't attempt to save locale if not authenticated [`#2025`](https://github.com/Human-Connection/Human-Connection/pull/2025)
@ -515,11 +623,11 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- build(deps-dev): bump @storybook/addon-a11y from 5.2.4 to 5.2.5 in /webapp [`#1989`](https://github.com/Human-Connection/Human-Connection/pull/1989)
- build(deps-dev): bump @vue/cli-shared-utils from 4.0.4 to 4.0.5 in /webapp [`#2002`](https://github.com/Human-Connection/Human-Connection/pull/2002)
- Update to version 0.1.7 [`#2015`](https://github.com/Human-Connection/Human-Connection/pull/2015)
- Update to version 0.1.8 [`d45264b`](https://github.com/Human-Connection/Human-Connection/commit/d45264b3afa1557c2205e7ca1b77c778ee37ab5a)
- build(deps): bump @nuxtjs/apollo in /webapp [`26c21b5`](https://github.com/Human-Connection/Human-Connection/commit/26c21b5b76c96206d98ff6bbfdbd1ca973ffcd4f)
- Finish redesign of moderators report list [`15d28aa`](https://github.com/Human-Connection/Human-Connection/commit/15d28aa8ef84788aa640aac67838380bfacf63b7)
- build(deps-dev): bump @storybook/addon-actions in /webapp [`7e95d37`](https://github.com/Human-Connection/Human-Connection/commit/7e95d376a311a5ede6351d577d30e25aea9cb65d)
#### [0.1.7](https://github.com/Human-Connection/Human-Connection/compare/0.1.6...0.1.7)
#### [v0.1.7](https://github.com/Human-Connection/Human-Connection/compare/v0.1.6...v0.1.7)
> 23 October 2019
@ -535,7 +643,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- build(deps-dev): bump @vue/cli-shared-utils in /webapp [`a5d993c`](https://github.com/Human-Connection/Human-Connection/commit/a5d993c761b2f92c3f44b6f83592ea4c1d822606)
- Fix block user workflow [`44e5437`](https://github.com/Human-Connection/Human-Connection/commit/44e54372c4148fafae1095d172d1a52a87b3b1b2)
#### [0.1.6](https://github.com/Human-Connection/Human-Connection/compare/0.1.5...0.1.6)
#### [v0.1.6](https://github.com/Human-Connection/Human-Connection/compare/v0.1.5...v0.1.6)
> 22 October 2019
@ -569,7 +677,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Refactor tests for querying reported resources [`4e42017`](https://github.com/Human-Connection/Human-Connection/commit/4e42017afaa97fa87ec726a5bbd1605cca911375)
- Return pinnedAt date from pinPost resolver/clean up [`be0c804`](https://github.com/Human-Connection/Human-Connection/commit/be0c8044e87e211f2578df151d9d2d11795a135f)
#### [0.1.5](https://github.com/Human-Connection/Human-Connection/compare/0.1.4...0.1.5)
#### [v0.1.5](https://github.com/Human-Connection/Human-Connection/compare/v0.1.4...v0.1.5)
> 17 October 2019
@ -627,7 +735,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Start adding missing portuguese translation [`33eb000`](https://github.com/Human-Connection/Human-Connection/commit/33eb000ee33e5aa513083450f0a00abd7240efb0)
- refactor: restructure translations and components [`bb5d581`](https://github.com/Human-Connection/Human-Connection/commit/bb5d581906b5e6e723966c3dc687c7f309356841)
#### [0.1.4](https://github.com/Human-Connection/Human-Connection/compare/0.1.3...0.1.4)
#### [v0.1.4](https://github.com/Human-Connection/Human-Connection/compare/v0.1.3...v0.1.4)
> 10 October 2019
@ -665,7 +773,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Fix lint, update tests [`bced698`](https://github.com/Human-Connection/Human-Connection/commit/bced6983ea1f51736e989eab6a41166723a6a6ca)
- add test embeds and links [`7cc139e`](https://github.com/Human-Connection/Human-Connection/commit/7cc139e879ac7ea912e82ea7eff14f7b67eddb4a)
#### [0.1.3](https://github.com/Human-Connection/Human-Connection/compare/0.1.2...0.1.3)
#### [v0.1.3](https://github.com/Human-Connection/Human-Connection/compare/v0.1.2...v0.1.3)
> 4 October 2019
@ -685,7 +793,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Set hasMore to false when returned Posts are equal to pageSize [`6f1c5e3`](https://github.com/Human-Connection/Human-Connection/commit/6f1c5e3efa3b77e72172592a0b5e4ea52158e642)
- refactor: use named slot for additional text [`3912b21`](https://github.com/Human-Connection/Human-Connection/commit/3912b21ea2f24e2e25682060b7166d1511442e6e)
#### [0.1.2](https://github.com/Human-Connection/Human-Connection/compare/0.1.1...0.1.2)
#### [v0.1.2](https://github.com/Human-Connection/Human-Connection/compare/v0.1.1...v0.1.2)
> 2 October 2019
@ -744,7 +852,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- build(deps): bump @nuxtjs/apollo in /webapp [`4648080`](https://github.com/Human-Connection/Human-Connection/commit/4648080a74fa6df60d6bb9b34d1db5030a9d4124)
- Write and refactor backend test which are supposed to fail at first [`6ad9dc2`](https://github.com/Human-Connection/Human-Connection/commit/6ad9dc27e937eb263914846c073172906aa661e1)
#### [0.1.1](https://github.com/Human-Connection/Human-Connection/compare/0.1.0...0.1.1)
#### [v0.1.1](https://github.com/Human-Connection/Human-Connection/compare/v0.1.0...v0.1.1)
> 27 September 2019
@ -818,7 +926,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Run with tag 0.1.0 [`c634ad2`](https://github.com/Human-Connection/Human-Connection/commit/c634ad264bd99dd1a87a86f870d7877aa751dc38)
- zwischenspeichern [`e4c7c11`](https://github.com/Human-Connection/Human-Connection/commit/e4c7c1125da6f8fa259241b4d3838b1e7b1e24a2)
#### 0.1.0
#### v0.1.0
> 18 September 2019
@ -1956,5 +2064,5 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Merge pull request #93 from Gerald1614/500_error_on_login [`#49`](https://github.com/Human-Connection/Human-Connection/issues/49)
- Update schema.graphql [`#7`](https://github.com/Human-Connection/Human-Connection/issues/7)
- Refactore the import and hashtags to all unicode characters [`0bc4c55`](https://github.com/Human-Connection/Human-Connection/commit/0bc4c558ae8f01d6d975b8ee1ea7f0f42b056d91)
- Change strategy, only build docker image [`d6b7374`](https://github.com/Human-Connection/Human-Connection/commit/d6b7374ddbf497bdb5cbc935b88ae085c38b3237)
- Copy package.json from webapp/ [`f3a9996`](https://github.com/Human-Connection/Human-Connection/commit/f3a9996962e5dd8b2e365a032c1a5766fe666103)
- Remove package-lock.json [`25bd96e`](https://github.com/Human-Connection/Human-Connection/commit/25bd96eedf6be5b7ea6e94c8433d044e13d62e70)
- Remove Styleguide [`53ea934`](https://github.com/Human-Connection/Human-Connection/commit/53ea93492dcc7f861743cd50a4ddf7728c9d659b)

View File

@ -32,6 +32,7 @@
* [Volume Snapshots](deployment/volumes/volume-snapshots/README.md)
* [Reclaim Policy](deployment/volumes/reclaim-policy/README.md)
* [Velero](deployment/volumes/velero/README.md)
* [Metrics](deployment/monitoring/README.md)
* [Legacy Migration](deployment/legacy-migration/README.md)
* [Feature Specification](cypress/features.md)
* [Code of conduct](CODE_OF_CONDUCT.md)

View File

@ -1 +0,0 @@
0.2.1

View File

@ -1,13 +1,13 @@
{
"name": "human-connection-backend",
"version": "0.0.1",
"version": "0.2.2",
"description": "GraphQL Backend for Human Connection",
"main": "src/index.js",
"scripts": {
"build": "babel src/ -d dist/ --copy-files",
"start": "node dist/",
"dev": "nodemon --exec babel-node src/ -e js,gql",
"dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/index.js -e js,gql",
"dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/ -e js,gql",
"lint": "eslint src --config .eslintrc.js",
"test": "jest --forceExit --detectOpenHandles --runInBand",
"db:reset": "babel-node src/seed/reset-db.js",
@ -32,7 +32,7 @@
]
},
"dependencies": {
"@hapi/joi": "^17.0.2",
"@hapi/joi": "^17.1.0",
"@sentry/node": "^5.11.1",
"apollo-cache-inmemory": "~1.6.5",
"apollo-client": "~2.6.8",
@ -62,24 +62,24 @@
"linkifyjs": "~2.1.8",
"lodash": "~4.17.14",
"merge-graphql-schemas": "^1.7.6",
"metascraper": "^5.10.2",
"metascraper-audio": "^5.9.5",
"metascraper-author": "^5.9.5",
"metascraper": "^5.10.5",
"metascraper-audio": "^5.10.5",
"metascraper-author": "^5.10.5",
"metascraper-clearbit-logo": "^5.3.0",
"metascraper-date": "^5.10.3",
"metascraper-description": "^5.9.5",
"metascraper-image": "^5.9.5",
"metascraper-lang": "^5.9.5",
"metascraper-date": "^5.10.5",
"metascraper-description": "^5.10.5",
"metascraper-image": "^5.10.5",
"metascraper-lang": "^5.10.5",
"metascraper-lang-detector": "^4.10.2",
"metascraper-logo": "^5.9.5",
"metascraper-publisher": "^5.10.3",
"metascraper-soundcloud": "^5.9.5",
"metascraper-title": "^5.10.3",
"metascraper-url": "^5.9.5",
"metascraper-video": "^5.9.5",
"metascraper-youtube": "^5.9.5",
"metascraper-logo": "^5.10.5",
"metascraper-publisher": "^5.10.5",
"metascraper-soundcloud": "^5.10.5",
"metascraper-title": "^5.10.5",
"metascraper-url": "^5.10.5",
"metascraper-video": "^5.10.5",
"metascraper-youtube": "^5.10.5",
"minimatch": "^3.0.4",
"mustache": "^3.2.1",
"mustache": "^4.0.0",
"neo4j-driver": "^4.0.1",
"neo4j-graphql-js": "^2.11.5",
"neode": "^0.3.7",
@ -88,12 +88,12 @@
"nodemailer-html-to-text": "^3.1.0",
"npm-run-all": "~4.1.5",
"request": "~2.88.0",
"sanitize-html": "~1.20.1",
"sanitize-html": "~1.21.1",
"slug": "~2.1.0",
"trunc-html": "~1.1.2",
"uuid": "~3.3.3",
"uuid": "~3.4.0",
"validator": "^12.1.0",
"wait-on": "~3.3.0",
"wait-on": "~4.0.0",
"xregexp": "^4.2.4"
},
"devDependencies": {
@ -118,7 +118,7 @@
"eslint-plugin-prettier": "~3.1.2",
"eslint-plugin-promise": "~4.2.1",
"eslint-plugin-standard": "~4.0.1",
"jest": "~24.9.0",
"jest": "~25.1.0",
"nodemon": "~2.0.2",
"prettier": "~1.19.1",
"supertest": "~4.0.2"

View File

@ -1,7 +1,8 @@
import dotenv from 'dotenv'
import path from 'path'
dotenv.config({ path: path.resolve(__dirname, '../../.env') })
if (require.resolve) {
// are we in a nodejs environment?
dotenv.config({ path: require.resolve('../../.env') })
}
const {
MAPBOX_TOKEN,
@ -27,6 +28,15 @@ export const requiredConfigs = {
PRIVATE_KEY_PASSPHRASE,
}
if (require.resolve) {
// are we in a nodejs environment?
Object.entries(requiredConfigs).map(entry => {
if (!entry[1]) {
throw new Error(`ERROR: "${entry[0]}" env variable is missing.`)
}
})
}
export const smtpConfigs = {
SMTP_HOST,
SMTP_PORT,

View File

@ -101,7 +101,7 @@ export default shield(
Badge: allow,
PostsEmotionsCountByEmotion: allow,
PostsEmotionsByCurrentUser: isAuthenticated,
blockedUsers: isAuthenticated,
mutedUsers: isAuthenticated,
notifications: isAuthenticated,
Donations: isAuthenticated,
},
@ -137,8 +137,8 @@ export default shield(
resetPassword: allow,
AddPostEmotions: isAuthenticated,
RemovePostEmotions: isAuthenticated,
block: isAuthenticated,
unblock: isAuthenticated,
muteUser: isAuthenticated,
unmuteUser: isAuthenticated,
markAsRead: isAuthenticated,
AddEmailAddress: isAuthenticated,
VerifyEmailAddress: isAuthenticated,

View File

@ -1,4 +1,4 @@
module.exports = {
export default {
id: { type: 'string', primary: true, lowercase: true },
status: { type: 'string', valid: ['permanent', 'temporary'] },
type: { type: 'string', valid: ['role', 'crowdfunding'] },

View File

@ -1,6 +1,6 @@
import uuid from 'uuid/v4'
module.exports = {
export default {
id: { type: 'string', primary: true, default: uuid },
name: { type: 'string', required: true, default: false },
slug: { type: 'string' },

View File

@ -1,6 +1,6 @@
import uuid from 'uuid/v4'
module.exports = {
export default {
id: { type: 'string', primary: true, default: uuid },
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
updatedAt: {

View File

@ -1,6 +1,6 @@
import uuid from 'uuid/v4'
module.exports = {
export default {
id: { type: 'string', primary: true, default: uuid },
goal: { type: 'number' },
progress: { type: 'number' },

View File

@ -1,4 +1,4 @@
module.exports = {
export default {
email: { type: 'string', primary: true, lowercase: true, email: true },
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
verifiedAt: { type: 'string', isoDate: true },

View File

@ -1,4 +1,4 @@
module.exports = {
export default {
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
token: { type: 'string', primary: true, token: true },
generatedBy: {

View File

@ -1,4 +1,4 @@
module.exports = {
export default {
id: { type: 'string', primary: true },
lat: { type: 'number' },
lng: { type: 'number' },

View File

@ -1,6 +1,6 @@
import uuid from 'uuid/v4'
module.exports = {
export default {
id: { type: 'string', primary: true, default: uuid },
activityId: { type: 'string', allow: [null] },
objectId: { type: 'string', allow: [null] },

View File

@ -1,6 +1,6 @@
import uuid from 'uuid/v4'
module.exports = {
export default {
id: { type: 'string', primary: true, default: uuid },
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
updatedAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },

View File

@ -1,6 +1,6 @@
import uuid from 'uuid/v4'
module.exports = {
export default {
id: { type: 'string', primary: true, default: uuid },
url: { type: 'string', uri: true, required: true },
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },

View File

@ -1,4 +1,4 @@
module.exports = {
export default {
id: { type: 'string', primary: true },
deleted: { type: 'boolean', default: false },
disabled: { type: 'boolean', default: false },

View File

@ -1,4 +1,4 @@
module.exports = {
export default {
email: { type: 'string', primary: true, lowercase: true, email: true },
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
nonce: { type: 'string', token: true },

View File

@ -1,6 +1,6 @@
import uuid from 'uuid/v4'
module.exports = {
export default {
id: { type: 'string', primary: true, default: uuid }, // TODO: should be type: 'uuid' but simplified for our tests
actorId: { type: 'string', allow: [null] },
name: { type: 'string', disallow: [null], min: 3 },
@ -78,6 +78,12 @@ module.exports = {
target: 'User',
direction: 'out',
},
muted: {
type: 'relationship',
relationship: 'MUTED',
target: 'User',
direction: 'out',
},
notifications: {
type: 'relationship',
relationship: 'NOTIFIED',

View File

@ -1,16 +1,16 @@
// NOTE: We cannot use `fs` here to clean up the code. Cypress breaks on any npm
// module that is not browser-compatible. Node's `fs` module is server-side only
export default {
Badge: require('./Badge.js'),
User: require('./User.js'),
EmailAddress: require('./EmailAddress.js'),
UnverifiedEmailAddress: require('./UnverifiedEmailAddress.js'),
SocialMedia: require('./SocialMedia.js'),
Post: require('./Post.js'),
Comment: require('./Comment.js'),
Category: require('./Category.js'),
Tag: require('./Tag.js'),
Location: require('./Location.js'),
Donations: require('./Donations.js'),
Report: require('./Report.js'),
Badge: require('./Badge.js').default,
User: require('./User.js').default,
EmailAddress: require('./EmailAddress.js').default,
UnverifiedEmailAddress: require('./UnverifiedEmailAddress.js').default,
SocialMedia: require('./SocialMedia.js').default,
Post: require('./Post.js').default,
Comment: require('./Comment.js').default,
Category: require('./Category.js').default,
Tag: require('./Tag.js').default,
Location: require('./Location.js').default,
Donations: require('./Donations.js').default,
Report: require('./Report.js').default,
}

View File

@ -1,25 +0,0 @@
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

@ -0,0 +1,22 @@
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,10 +1,10 @@
import uuid from 'uuid/v4'
import { neo4jgraphql } from 'neo4j-graphql-js'
import { isEmpty } from 'lodash'
import fileUpload from './fileUpload'
import { UserInputError } from 'apollo-server'
import fileUpload from './fileUpload'
import Resolver from './helpers/Resolver'
import { filterForBlockedUsers } from './helpers/filterForBlockedUsers'
import { filterForMutedUsers } from './helpers/filterForMutedUsers'
const maintainPinnedPosts = params => {
const pinnedPostFilter = { pinned: true }
@ -19,16 +19,16 @@ const maintainPinnedPosts = params => {
export default {
Query: {
Post: async (object, params, context, resolveInfo) => {
params = await filterForBlockedUsers(params, context)
params = await filterForMutedUsers(params, context)
params = await maintainPinnedPosts(params)
return neo4jgraphql(object, params, context, resolveInfo)
},
findPosts: async (object, params, context, resolveInfo) => {
params = await filterForBlockedUsers(params, context)
params = await filterForMutedUsers(params, context)
return neo4jgraphql(object, params, context, resolveInfo)
},
profilePagePosts: async (object, params, context, resolveInfo) => {
params = await filterForBlockedUsers(params, context)
params = await filterForMutedUsers(params, context)
return neo4jgraphql(object, params, context, resolveInfo)
},
PostsEmotionsCountByEmotion: async (object, params, context, resolveInfo) => {

View File

@ -8,42 +8,26 @@ import createOrUpdateLocations from './users/location'
const neode = getNeode()
export const getBlockedUsers = async context => {
export const getMutedUsers = async context => {
const { neode } = context
const userModel = neode.model('User')
let blockedUsers = neode
let mutedUsers = neode
.query()
.match('user', userModel)
.where('user.id', context.user.id)
.relationship(userModel.relationships().get('blocked'))
.to('blocked', userModel)
.return('blocked')
blockedUsers = await blockedUsers.execute()
blockedUsers = blockedUsers.records.map(r => r.get('blocked').properties)
return blockedUsers
}
export const getBlockedByUsers = async context => {
if (context.user.role === 'moderator' || context.user.role === 'admin') return []
const { neode } = context
const userModel = neode.model('User')
let blockedByUsers = neode
.query()
.match('user', userModel)
.relationship(userModel.relationships().get('blocked'))
.to('blocked', userModel)
.where('blocked.id', context.user.id)
.return('user')
blockedByUsers = await blockedByUsers.execute()
blockedByUsers = blockedByUsers.records.map(r => r.get('user').properties)
return blockedByUsers
.relationship(userModel.relationships().get('muted'))
.to('muted', userModel)
.return('muted')
mutedUsers = await mutedUsers.execute()
mutedUsers = mutedUsers.records.map(r => r.get('muted').properties)
return mutedUsers
}
export default {
Query: {
blockedUsers: async (object, args, context, resolveInfo) => {
mutedUsers: async (object, args, context, resolveInfo) => {
try {
return getBlockedUsers(context)
return getMutedUsers(context)
} catch (e) {
throw new UserInputError(e.message)
}
@ -72,6 +56,36 @@ export default {
},
},
Mutation: {
muteUser: async (_parent, params, context, _resolveInfo) => {
const { user: currentUser } = context
if (currentUser.id === params.id) return null
await neode.cypher(
`
MATCH(u:User {id: $currentUser.id})-[previousRelationship:FOLLOWS]->(b:User {id: $params.id})
DELETE previousRelationship
`,
{ currentUser, params },
)
const [user, mutedUser] = await Promise.all([
neode.find('User', currentUser.id),
neode.find('User', params.id),
])
await user.relateTo(mutedUser, 'muted')
return mutedUser.toJson()
},
unmuteUser: async (_parent, params, context, _resolveInfo) => {
const { user: currentUser } = context
if (currentUser.id === params.id) return null
await neode.cypher(
`
MATCH(u:User {id: $currentUser.id})-[previousRelationship:MUTED]->(b:User {id: $params.id})
DELETE previousRelationship
`,
{ currentUser, params },
)
const unmutedUser = await neode.find('User', params.id)
return unmutedUser.toJson()
},
block: async (object, args, context, resolveInfo) => {
const { user: currentUser } = context
if (currentUser.id === args.id) return null
@ -217,6 +231,8 @@ export default {
'MATCH (this)<-[:FOLLOWS]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
isBlocked:
'MATCH (this)<-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
isMuted:
'MATCH (this)<-[:MUTED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
},
count: {
contributionsCount:

View File

@ -9,7 +9,7 @@ const factory = Factory()
const neode = getNeode()
let currentUser
let blockedUser
let mutedUser
let authenticatedUser
let server
@ -33,15 +33,15 @@ afterEach(async () => {
await factory.cleanDatabase()
})
describe('blockedUsers', () => {
let blockedUserQuery
describe('mutedUsers', () => {
let mutedUserQuery
beforeEach(() => {
blockedUserQuery = gql`
mutedUserQuery = gql`
query {
blockedUsers {
mutedUsers {
id
name
isBlocked
isMuted
}
}
`
@ -49,34 +49,34 @@ describe('blockedUsers', () => {
it('throws permission error', async () => {
const { query } = createTestClient(server)
const result = await query({ query: blockedUserQuery })
const result = await query({ query: mutedUserQuery })
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
})
describe('authenticated and given a blocked user', () => {
describe('authenticated and given a muted user', () => {
beforeEach(async () => {
currentUser = await neode.create('User', {
name: 'Current User',
id: 'u1',
})
blockedUser = await neode.create('User', {
name: 'Blocked User',
mutedUser = await neode.create('User', {
name: 'Muted User',
id: 'u2',
})
await currentUser.relateTo(blockedUser, 'blocked')
await currentUser.relateTo(mutedUser, 'muted')
authenticatedUser = await currentUser.toJson()
})
it('returns a list of blocked users', async () => {
it('returns a list of muted users', async () => {
const { query } = createTestClient(server)
await expect(query({ query: blockedUserQuery })).resolves.toEqual(
await expect(query({ query: mutedUserQuery })).resolves.toEqual(
expect.objectContaining({
data: {
blockedUsers: [
mutedUsers: [
{
name: 'Blocked User',
name: 'Muted User',
id: 'u2',
isBlocked: true,
isMuted: true,
},
],
},
@ -86,28 +86,28 @@ describe('blockedUsers', () => {
})
})
describe('block', () => {
let blockAction
describe('muteUser', () => {
let muteAction
beforeEach(() => {
currentUser = undefined
blockAction = variables => {
muteAction = variables => {
const { mutate } = createTestClient(server)
const blockMutation = gql`
const muteUserMutation = gql`
mutation($id: ID!) {
block(id: $id) {
muteUser(id: $id) {
id
name
isBlocked
isMuted
}
}
`
return mutate({ mutation: blockMutation, variables })
return mutate({ mutation: muteUserMutation, variables })
}
})
it('throws permission error', async () => {
const result = await blockAction({ id: 'u2' })
const result = await muteAction({ id: 'u2' })
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
})
@ -120,45 +120,47 @@ describe('block', () => {
authenticatedUser = await currentUser.toJson()
})
describe('block yourself', () => {
describe('mute yourself', () => {
it('returns null', async () => {
await expect(blockAction({ id: 'u1' })).resolves.toEqual(
expect.objectContaining({ data: { block: null } }),
await expect(muteAction({ id: 'u1' })).resolves.toEqual(
expect.objectContaining({ data: { muteUser: null } }),
)
})
})
describe('block not existing user', () => {
describe('mute not existing user', () => {
it('returns null', async () => {
await expect(blockAction({ id: 'u2' })).resolves.toEqual(
expect.objectContaining({ data: { block: null } }),
await expect(muteAction({ id: 'u2' })).resolves.toEqual(
expect.objectContaining({ data: { muteUser: null } }),
)
})
})
describe('given a to-be-blocked user', () => {
describe('given a to-be-muted user', () => {
beforeEach(async () => {
blockedUser = await neode.create('User', {
name: 'Blocked User',
mutedUser = await neode.create('User', {
name: 'Muted User',
id: 'u2',
})
})
it('blocks a user', async () => {
await expect(blockAction({ id: 'u2' })).resolves.toEqual(
it('mutes a user', async () => {
await expect(muteAction({ id: 'u2' })).resolves.toEqual(
expect.objectContaining({
data: { block: { id: 'u2', name: 'Blocked User', isBlocked: true } },
data: {
muteUser: { id: 'u2', name: 'Muted User', isMuted: true },
},
}),
)
})
it('unfollows the user', async () => {
await currentUser.relateTo(blockedUser, 'following')
await currentUser.relateTo(mutedUser, 'following')
const queryUser = gql`
query {
User(id: "u2") {
id
isBlocked
isMuted
followedByCurrentUser
}
}
@ -166,18 +168,18 @@ describe('block', () => {
const { query } = createTestClient(server)
await expect(query({ query: queryUser })).resolves.toEqual(
expect.objectContaining({
data: { User: [{ id: 'u2', isBlocked: false, followedByCurrentUser: true }] },
data: { User: [{ id: 'u2', isMuted: false, followedByCurrentUser: true }] },
}),
)
await blockAction({ id: 'u2' })
await muteAction({ id: 'u2' })
await expect(query({ query: queryUser })).resolves.toEqual(
expect.objectContaining({
data: { User: [{ id: 'u2', isBlocked: true, followedByCurrentUser: false }] },
data: { User: [{ id: 'u2', isMuted: true, followedByCurrentUser: false }] },
}),
)
})
describe('given both the current user and the to-be-blocked user write a post', () => {
describe('given both the current user and the to-be-muted user write a post', () => {
let postQuery
beforeEach(async () => {
@ -187,11 +189,11 @@ describe('block', () => {
})
const post2 = await neode.create('Post', {
id: 'p23',
title: 'A post written by the blocked user',
title: 'A post written by the muted user',
})
await Promise.all([
post1.relateTo(currentUser, 'author'),
post2.relateTo(blockedUser, 'author'),
post2.relateTo(mutedUser, 'author'),
])
postQuery = gql`
query {
@ -223,9 +225,9 @@ describe('block', () => {
},
{
id: 'p23',
title: 'A post written by the blocked user',
title: 'A post written by the muted user',
author: {
name: 'Blocked User',
name: 'Muted User',
id: 'u2',
},
},
@ -238,12 +240,12 @@ describe('block', () => {
describe('from the perspective of the current user', () => {
it('both posts are in the newsfeed', bothPostsAreInTheNewsfeed)
describe('but if the current user blocks the other user', () => {
describe('but if the current user mutes the other user', () => {
beforeEach(async () => {
await currentUser.relateTo(blockedUser, 'blocked')
await currentUser.relateTo(mutedUser, 'muted')
})
it("the blocked user's post won't show up in the newsfeed of the current user", async () => {
it("the muted user's post won't show up in the newsfeed of the current user", async () => {
const { query } = createTestClient(server)
await expect(query({ query: postQuery })).resolves.toEqual(
expect.objectContaining({
@ -262,29 +264,34 @@ describe('block', () => {
})
})
describe('from the perspective of the blocked user', () => {
describe('from the perspective of the muted user', () => {
beforeEach(async () => {
authenticatedUser = await blockedUser.toJson()
authenticatedUser = await mutedUser.toJson()
})
it('both posts are in the newsfeed', bothPostsAreInTheNewsfeed)
describe('but if the current user blocks the other user', () => {
describe('but if the current user mutes the other user', () => {
beforeEach(async () => {
await currentUser.relateTo(blockedUser, 'blocked')
await currentUser.relateTo(mutedUser, 'muted')
})
it("the current user's post won't show up in the newsfeed of the blocked user", async () => {
it("the current user's post will show up in the newsfeed of the muted user", async () => {
const { query } = createTestClient(server)
await expect(query({ query: postQuery })).resolves.toEqual(
expect.objectContaining({
data: {
Post: [
Post: expect.arrayContaining([
{
id: 'p23',
title: 'A post written by the blocked user',
author: { name: 'Blocked User', id: 'u2' },
title: 'A post written by the muted user',
author: { name: 'Muted User', id: 'u2' },
},
],
{
id: 'p12',
title: 'A post written by the current user',
author: { name: 'Current User', id: 'u1' },
},
]),
},
}),
)
@ -296,28 +303,28 @@ describe('block', () => {
})
})
describe('unblock', () => {
let unblockAction
describe('unmuteUser', () => {
let unmuteAction
beforeEach(() => {
currentUser = undefined
unblockAction = variables => {
unmuteAction = variables => {
const { mutate } = createTestClient(server)
const unblockMutation = gql`
const unmuteUserMutation = gql`
mutation($id: ID!) {
unblock(id: $id) {
unmuteUser(id: $id) {
id
name
isBlocked
isMuted
}
}
`
return mutate({ mutation: unblockMutation, variables })
return mutate({ mutation: unmuteUserMutation, variables })
}
})
it('throws permission error', async () => {
const result = await unblockAction({ id: 'u2' })
const result = await unmuteAction({ id: 'u2' })
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
})
@ -330,59 +337,69 @@ describe('unblock', () => {
authenticatedUser = await currentUser.toJson()
})
describe('unblock yourself', () => {
describe('unmute yourself', () => {
it('returns null', async () => {
await expect(unblockAction({ id: 'u1' })).resolves.toEqual(
expect.objectContaining({ data: { unblock: null } }),
await expect(unmuteAction({ id: 'u1' })).resolves.toEqual(
expect.objectContaining({ data: { unmuteUser: null } }),
)
})
})
describe('unblock not-existing user', () => {
describe('unmute not-existing user', () => {
it('returns null', async () => {
await expect(unblockAction({ id: 'lksjdflksfdj' })).resolves.toEqual(
expect.objectContaining({ data: { unblock: null } }),
await expect(unmuteAction({ id: 'lksjdflksfdj' })).resolves.toEqual(
expect.objectContaining({ data: { unmuteUser: null } }),
)
})
})
describe('given another user', () => {
beforeEach(async () => {
blockedUser = await neode.create('User', {
name: 'Blocked User',
mutedUser = await neode.create('User', {
name: 'Muted User',
id: 'u2',
})
})
describe('unblocking a not yet blocked user', () => {
describe('unmuting a not yet muted user', () => {
it('does not hurt', async () => {
await expect(unblockAction({ id: 'u2' })).resolves.toEqual(
await expect(unmuteAction({ id: 'u2' })).resolves.toEqual(
expect.objectContaining({
data: { unblock: { id: 'u2', name: 'Blocked User', isBlocked: false } },
data: {
unmuteUser: { id: 'u2', name: 'Muted User', isMuted: false },
},
}),
)
})
})
describe('given a blocked user', () => {
describe('given a muted user', () => {
beforeEach(async () => {
await currentUser.relateTo(blockedUser, 'blocked')
await currentUser.relateTo(mutedUser, 'muted')
})
it('unblocks a user', async () => {
await expect(unblockAction({ id: 'u2' })).resolves.toEqual(
it('unmutes a user', async () => {
await expect(unmuteAction({ id: 'u2' })).resolves.toEqual(
expect.objectContaining({
data: { unblock: { id: 'u2', name: 'Blocked User', isBlocked: false } },
data: {
unmuteUser: { id: 'u2', name: 'Muted User', isMuted: false },
},
}),
)
})
describe('unblocking twice', () => {
describe('unmuting twice', () => {
it('has no effect', async () => {
await unblockAction({ id: 'u2' })
await expect(unblockAction({ id: 'u2' })).resolves.toEqual(
await unmuteAction({ id: 'u2' })
await expect(unmuteAction({ id: 'u2' })).resolves.toEqual(
expect.objectContaining({
data: { unblock: { id: 'u2', name: 'Blocked User', isBlocked: false } },
data: {
unmuteUser: {
id: 'u2',
name: 'Muted User',
isMuted: false,
},
},
}),
)
})

View File

@ -1,30 +1,5 @@
import fs from 'fs'
import path from 'path'
import { mergeTypes } from 'merge-graphql-schemas'
const findGqlFiles = dir => {
var results = []
var list = fs.readdirSync(dir)
list.forEach(file => {
file = path.join(dir, file).toString('utf-8')
var stat = fs.statSync(file)
if (stat && stat.isDirectory()) {
// Recurse into a subdirectory
results = results.concat(findGqlFiles(file))
} else {
if (path.extname(file) === '.gql') {
// Is a gql file
results.push(file)
}
}
})
return results
}
const typeDefs = []
findGqlFiles(__dirname).forEach(file => {
typeDefs.push(fs.readFileSync(file).toString('utf-8'))
})
import { mergeTypes, fileLoader } from 'merge-graphql-schemas'
const typeDefs = fileLoader(path.join(__dirname, './**/*.gql'))
export default mergeTypes(typeDefs, { all: true })

View File

@ -75,6 +75,13 @@ type User {
"""
)
isMuted: Boolean! @cypher(
statement: """
MATCH (this)<-[:MUTED]-(user:User { id: $cypherParams.currentUserId})
RETURN COUNT(user) >= 1
"""
)
# contributions: [WrittenPost]!
# contributions2(first: Int = 10, offset: Int = 0): [WrittenPost2]!
# @cypher(
@ -160,6 +167,7 @@ type Query {
filter: _UserFilter
): [User]
mutedUsers: [User]
blockedUsers: [User]
isLoggedIn: Boolean!
currentUser: User
@ -197,7 +205,8 @@ type Mutation {
DeleteUser(id: ID!, resource: [Deletable]): User
muteUser(id: ID!): User
unmuteUser(id: ID!): User
block(id: ID!): User
unblock(id: ID!): User
}

View File

@ -1,30 +1,18 @@
import { getDriver, getNeode } from '../../bootstrap/neo4j'
import createBadge from './badges.js'
import createUser from './users.js'
import createPost from './posts.js'
import createComment from './comments.js'
import createCategory from './categories.js'
import createTag from './tags.js'
import createSocialMedia from './socialMedia.js'
import createLocation from './locations.js'
import createEmailAddress from './emailAddresses.js'
import createDonations from './donations.js'
import createUnverifiedEmailAddresss from './unverifiedEmailAddresses.js'
import createReport from './reports.js'
const factories = {
Badge: createBadge,
User: createUser,
Post: createPost,
Comment: createComment,
Category: createCategory,
Tag: createTag,
SocialMedia: createSocialMedia,
Location: createLocation,
EmailAddress: createEmailAddress,
UnverifiedEmailAddress: createUnverifiedEmailAddresss,
Donations: createDonations,
Report: createReport,
Badge: require('./badges.js').default,
User: require('./users.js').default,
Post: require('./posts.js').default,
Comment: require('./comments.js').default,
Category: require('./categories.js').default,
Tag: require('./tags.js').default,
SocialMedia: require('./socialMedia.js').default,
Location: require('./locations.js').default,
EmailAddress: require('./emailAddresses.js').default,
UnverifiedEmailAddress: require('./unverifiedEmailAddresses.js').default,
Donations: require('./donations.js').default,
Report: require('./reports.js').default,
}
export const cleanDatabase = async (options = {}) => {
@ -34,7 +22,7 @@ export const cleanDatabase = async (options = {}) => {
await session.writeTransaction(transaction => {
return transaction.run(
`
MATCH (everything)
MATCH (everything)
DETACH DELETE everything
`,
)

View File

@ -36,7 +36,6 @@ export default function create() {
if (categoryIds)
categories = await Promise.all(categoryIds.map(id => neodeInstance.find('Category', id)))
categories = categories || (await Promise.all([factoryInstance.create('Category')]))
const { tagIds = [] } = args
delete args.tags
const tags = await Promise.all(

View File

@ -226,6 +226,10 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
dewey.relateTo(huey, 'following'),
louie.relateTo(jennyRostock, 'following'),
huey.relateTo(dagobert, 'muted'),
dewey.relateTo(dagobert, 'muted'),
louie.relateTo(dagobert, 'muted'),
dagobert.relateTo(huey, 'blocked'),
dagobert.relateTo(dewey, 'blocked'),
dagobert.relateTo(louie, 'blocked'),

View File

@ -1,21 +1,13 @@
import express from 'express'
import helmet from 'helmet'
import { ApolloServer } from 'apollo-server-express'
import CONFIG, { requiredConfigs } from './config'
import CONFIG from './config'
import middleware from './middleware'
import { getNeode, getDriver } from './bootstrap/neo4j'
import decode from './jwt/decode'
import schema from './schema'
import webfinger from './activitypub/routes/webfinger'
// check required configs and throw error
// TODO check this directly in config file - currently not possible due to testsetup
Object.entries(requiredConfigs).map(entry => {
if (!entry[1]) {
throw new Error(`ERROR: "${entry[0]}" env variable is missing.`)
}
})
const driver = getDriver()
const neode = getNeode()

File diff suppressed because it is too large Load Diff

View File

@ -24,10 +24,10 @@ Then("my comment should be successfully created", () => {
Then("I should see my comment", () => {
cy.get("div.comment p")
.should("contain", "Human Connection rocks")
.get(".ds-avatar img")
.get(".user-avatar img")
.should("have.attr", "src")
.and("contain", narratorAvatar)
.get("div p.ds-text span")
.get(".user-teaser > .info > .text")
.should("contain", "today at");
});

View File

@ -32,5 +32,5 @@ Then("I cannot upload a picture", () => {
cy.get(".ds-card-content")
.children()
.should("not.have.id", "customdropzone")
.should("have.class", "ds-avatar");
.should("have.class", "user-avatar");
});

View File

@ -7,7 +7,7 @@ import { gql } from '../../../backend/src/helpers/jest'
let lastReportTitle
let davidIrvingPostTitle = 'The Truth about the Holocaust'
let davidIrvingPostSlug = 'the-truth-about-the-holocaust'
let annoyingUserWhoBlockedModeratorTitle = 'Fake news'
let annoyingUserWhoMutedModeratorTitle = 'Fake news'
const savePostTitle = $post => {
return $post
@ -63,7 +63,7 @@ When('I click on "Report User" from the content menu in the user info box', () =
})
When('I click on the author', () => {
cy.get('.username')
cy.get('.user-teaser')
.click()
.url().should('include', '/profile/')
})
@ -147,9 +147,9 @@ Then('I see all the reported posts including the one from above', () => {
})
})
Then('I see all the reported posts including from the user who blocked me', () => {
Then('I see all the reported posts including from the user who muted me', () => {
cy.get('table tbody').within(() => {
cy.contains('tr', annoyingUserWhoBlockedModeratorTitle)
cy.contains('tr', annoyingUserWhoMutedModeratorTitle)
})
})
@ -159,9 +159,9 @@ Then('each list item links to the post page', () => {
})
Then('I can visit the post page', () => {
cy.contains(annoyingUserWhoBlockedModeratorTitle).click()
cy.contains(annoyingUserWhoMutedModeratorTitle).click()
cy.location('pathname').should('contain', '/post')
.get('h3').should('contain', annoyingUserWhoBlockedModeratorTitle)
.get('h3').should('contain', annoyingUserWhoMutedModeratorTitle)
})
When("they have a post someone has reported", () => {

View File

@ -87,7 +87,7 @@ Then(
);
Then("I select a user entry", () => {
cy.get(".searchable-input .userinfo")
cy.get(".searchable-input .user-teaser")
.first()
.trigger("click");
})

View File

@ -29,10 +29,20 @@ const narratorParams = {
...termsAndConditionsAgreedVersion,
};
const annoyingParams = {
email: "spammy-spammer@example.org",
password: "1234",
...termsAndConditionsAgreedVersion
};
Given("I am logged in", () => {
cy.login(loginCredentials);
});
Given("I am logged in as the muted user", () => {
cy.login({ email: annoyingParams.email, password: '1234' });
});
Given("we have a selection of categories", () => {
cy.createCategories("cat0", "just-for-fun");
});
@ -227,7 +237,6 @@ Given("I previously created a post", () => {
lastPost.authorId = narratorParams.id
lastPost.title = "previously created post";
lastPost.content = "with some content";
lastPost.categoryIds = ["cat0"];
cy.factory()
.create("Post", lastPost);
});
@ -407,11 +416,6 @@ Then("there are no notifications in the top menu", () => {
});
Given("there is an annoying user called {string}", name => {
const annoyingParams = {
email: "spammy-spammer@example.org",
password: "1234",
...termsAndConditionsAgreedVersion
};
cy.factory().create("User", {
...annoyingParams,
id: "annoying-user",
@ -420,17 +424,17 @@ Given("there is an annoying user called {string}", name => {
});
});
Given("there is an annoying user who has blocked me", () => {
Given("there is an annoying user who has muted me", () => {
cy.neode()
.first("User", {
role: 'moderator'
})
.then(blocked => {
.then(mutedUser => {
cy.neode()
.first("User", {
id: 'annoying-user'
})
.relateTo(blocked, "blocked");
.relateTo(mutedUser, "muted");
});
});
@ -459,7 +463,7 @@ When(
);
When("I navigate to my {string} settings page", settingsPage => {
cy.get(".avatar-menu").click();
cy.get(".avatar-menu-trigger").click();
cy.get(".avatar-menu-popover")
.find("a[href]")
.contains("Settings")
@ -514,17 +518,17 @@ Given("I wrote a post {string}", title => {
});
});
When("I block the user {string}", name => {
When("I mute the user {string}", name => {
cy.neode()
.first("User", {
name
})
.then(blocked => {
.then(mutedUser => {
cy.neode()
.first("User", {
name: narratorParams.name
})
.relateTo(blocked, "blocked");
.relateTo(mutedUser, "muted");
});
});

View File

@ -11,7 +11,7 @@ Feature: Report and Moderate
Given we have the following user accounts:
| id | name |
| u67 | David Irving |
| annoying-user | I'm gonna block Moderators and Admins HA HA HA |
| annoying-user | I'm gonna mute Moderators and Admins HA HA HA |
Given we have the following posts in our database:
| authorId | id | title | content |
@ -58,16 +58,16 @@ Feature: Report and Moderate
Then I see all the reported posts including the one from above
And each list item links to the post page
Scenario: Review reported posts of a user who's blocked a moderator
Scenario: Review reported posts of a user who's muted a moderator
Given somebody reported the following posts:
| submitterEmail | resourceId | reasonCategory | reasonDescription |
| p2.submitter@example.org | p2 | other | Offensive content |
And my user account has the role "moderator"
And there is an annoying user who has blocked me
And there is an annoying user who has muted me
And I am logged in
When I click on the avatar menu in the top right corner
And I click on "Moderation"
Then I see all the reported posts including from the user who blocked me
Then I see all the reported posts including from the user who muted me
And I can visit the post page
Scenario: Normal user can't see the moderation page

View File

@ -1,22 +0,0 @@
Feature: Block a User
As a user
I'd like to have a button to block another user
To prevent him from seeing and interacting with my contributions and also to avoid seeing his/her posts
Background:
Given I have a user account
And there is an annoying user called "Spammy Spammer"
Scenario Outline: Blocked users cannot see each others posts
Given "Spammy Spammer" wrote a post "Spam Spam Spam"
And I wrote a post "I hate spammers"
And I block the user "Spammy Spammer"
When I log in with:
| Email | Password |
| <email> | <password> |
Then I see only one post with the title "<expected_title>"
Examples:
| email | password | expected_title |
| peterpan@example.org | 1234 | I hate spammers |
| spammy-spammer@example.org | 1234 | Spam Spam Spam |

View File

@ -1,6 +1,6 @@
Feature: Block a User
Feature: Mute a User
As a user
I'd like to have a button to block another user
I'd like to have a button to mute another user
To prevent him from seeing and interacting with my contributions and also to avoid seeing his/her posts
Background:
@ -8,36 +8,46 @@ Feature: Block a User
And there is an annoying user called "Spammy Spammer"
And I am logged in
Scenario: Block a user
Scenario: Mute a user
Given I am on the profile page of the annoying user
When I click on "Block user" from the content menu in the user info box
And I navigate to my "Blocked users" settings page
When I click on "Mute user" from the content menu in the user info box
And I navigate to my "Muted users" settings page
Then I can see the following table:
| Avatar | Name |
| | Spammy Spammer |
Scenario: Block a previously followed user
Scenario: Mute a previously followed user
Given I follow the user "Spammy Spammer"
And "Spammy Spammer" wrote a post "Spam Spam Spam"
When I visit the profile page of the annoying user
And I click on "Block user" from the content menu in the user info box
And I click on "Mute user" from the content menu in the user info box
Then the list of posts of this user is empty
And nobody is following the user profile anymore
Scenario: Posts of blocked users are filtered from search results
Scenario: Posts of muted users are filtered from search results
Given we have the following posts in our database:
| id | title | content |
| im-not-blocked | Post that should be seen | cause I'm not blocked |
| id | title | content |
| im-not-muted | Post that should be seen | cause I'm not muted |
Given "Spammy Spammer" wrote a post "Spam Spam Spam"
When I search for "Spam"
Then I should see the following posts in the select dropdown:
| title |
| Spam Spam Spam |
When I block the user "Spammy Spammer"
When I mute the user "Spammy Spammer"
And I refresh the page
And I search for "Spam"
Then the search has no results
But I search for "not blocked"
But I search for "not muted"
Then I should see the following posts in the select dropdown:
| title |
| Post that should be seen |
Scenario: Muted users can still see my posts
Given I previously created a post
And I mute the user "Spammy Spammer"
Given I log out
And I am logged in as the muted user
When I search for "previously created"
Then I should see the following posts in the select dropdown:
| title |
| previously created post |

View File

@ -11,10 +11,19 @@
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
const cucumber = require('cypress-cucumber-preprocessor').default
module.exports = on => {
const dotenv = require('dotenv')
module.exports = (on, config) => {
// (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
const { parsed } = dotenv.config({ path: require.resolve('../../backend/.env') })
config.env.NEO4J_URI = parsed.NEO4J_URI
config.env.NEO4J_USERNAME = parsed.NEO4J_USERNAME
config.env.NEO4J_PASSWORD = parsed.NEO4J_PASSWORD
on('file:preprocessor', cucumber())
return config
}

View File

@ -60,7 +60,7 @@ Cypress.Commands.add("login", ({ email, password }) => {
.as("submitButton")
.click();
cy.get(".iziToast-message").should("contain", "You are logged in!");
cy.get(".iziToast-close").click();
cy.location("pathname").should("eq", "/");
});
Cypress.Commands.add("logout", (email, password) => {

View File

@ -1,9 +1,13 @@
import Factory from '../../backend/src/seed/factories'
import { getDriver, getNeode } from '../../backend/src/bootstrap/neo4j'
import neode from 'neode'
const neo4jDriver = getDriver()
const neodeInstance = getNeode()
const neo4jConfigs = {
uri: Cypress.env('NEO4J_URI'),
username: Cypress.env('NEO4J_USERNAME'),
password: Cypress.env('NEO4J_PASSWORD')
}
const neo4jDriver = getDriver(neo4jConfigs)
const neodeInstance = getNeode(neo4jConfigs)
const factoryOptions = { neo4jDriver, neodeInstance }
const factory = Factory(factoryOptions)

View File

@ -0,0 +1,43 @@
# Metrics
You can optionally setup [prometheus](https://prometheus.io/) and
[grafana](https://grafana.com/) for metrics.
We follow this tutorial [here](https://medium.com/@chris_linguine/how-to-monitor-your-kubernetes-cluster-with-prometheus-and-grafana-2d5704187fc8):
```bash
kubectl proxy # proxy to your kubernetes dashboard
helm repo list
# If using helm v3, the stable repository is not set, so you need to manually add it.
helm repo add stable https://kubernetes-charts.storage.googleapis.com
# Create a monitoring namespace for your cluster
kubectl create namespace monitoring
helm --namespace monitoring install prometheus stable/prometheus
kubectl -n monitoring get pods # look for 'server'
kubectl port-forward -n monitoring <PROMETHEUS_SERVER_ID> 9090
# You can now see your prometheus server on: http://localhost:9090
# Make sure you are in folder `deployment/`
kubectl apply -f monitoring/grafana/config.yml
helm --namespace monitoring install grafana stable/grafana -f monitoring/grafana/values.yml
# Get the admin password for grafana from your kubernetes dashboard.
kubectl --namespace monitoring port-forward <POD_NAME> 3000
# You can now see your grafana dashboard on: http://localhost:3000
# Login with user 'admin' and the password you just looked up.
# In your dashboard import this dashboard:
# https://grafana.com/grafana/dashboards/1860
# Enter ID 180 and choose "Prometheus" as datasource.
# You got metrics!
```
Now you should see something like this:
![Grafana dashboard](./grafana/metrics.png)
You can set up a grafana dashboard, by visiting https://grafana.com/dashboards, finding one that is suitable and copying it's id.
You then go to the left hand menu in localhost, choose `Dashboard` > `Manage` > `Import`
Paste in the id, click `Load`, select `Prometheus` for the data source, and click `Import`
When you just installed prometheus and grafana, the data will not be available
immediately, so wait for a couple of minutes and reload.

View File

@ -0,0 +1,16 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: prometheus-grafana-datasource
namespace: monitoring
labels:
grafana_datasource: '1'
data:
datasource.yaml: |-
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
orgId: 1
url: http://prometheus-server.monitoring.svc.cluster.local

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

View File

@ -0,0 +1,4 @@
sidecar:
datasources:
enabled: true
label: grafana_datasource

View File

@ -1,9 +1,13 @@
{
"name": "human-connection",
"version": "0.2.1",
"version": "0.2.2",
"description": "Fullstack and API tests with cypress and cucumber for Human Connection",
"author": "Human Connection gGmbh",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/Human-Connection/Human-Connection.git"
},
"cypress-cucumber-preprocessor": {
"nonGlobalStepDefinitions": true
},
@ -14,11 +18,11 @@
"cypress:backend": "cd backend && yarn run dev",
"cypress:webapp": "cd webapp && yarn run dev",
"cypress:setup": "run-p cypress:backend cypress:webapp",
"cypress:run": "cross-env cypress run --browser chromium",
"cypress:open": "cross-env cypress open --browser chromium",
"cypress:run": "cross-env cypress run",
"cypress:open": "cross-env cypress open",
"cucumber:setup": "cd backend && yarn run dev",
"cucumber": "wait-on tcp:4000 && cucumber-js --require-module @babel/register --exit",
"version": "auto-changelog -p"
"release": "standard-version"
},
"devDependencies": {
"@babel/core": "^7.8.3",
@ -35,13 +39,14 @@
"cypress-plugin-retries": "^1.5.2",
"date-fns": "^2.9.0",
"dotenv": "^8.2.0",
"expect": "^24.9.0",
"expect": "^25.1.0",
"faker": "Marak/faker.js#master",
"graphql-request": "^1.8.2",
"neo4j-driver": "^4.0.1",
"neode": "^0.3.7",
"npm-run-all": "^4.1.5",
"slug": "^2.1.0"
"slug": "^2.1.0",
"standard-version": "^7.1.0"
},
"resolutions": {
"set-value": "^2.0.1"

View File

@ -2,7 +2,8 @@
ROOT_DIR=$(dirname "$0")/..
# BUILD_COMMIT=${TRAVIS_COMMIT:-$(git rev-parse HEAD)}
IFS='.' read -r major minor patch < $ROOT_DIR/VERSION
VERSION=$(jq -r '.version' $ROOT_DIR/package.json)
IFS='.' read -r major minor patch <<< $VERSION
apps=(nitro-web nitro-backend neo4j maintenance)
tags=($major $major.$minor $major.$minor.$patch)

View File

@ -2,7 +2,7 @@
ROOT_DIR=$(dirname "$0")/..
RELEASE_DIR="${ROOT_DIR}/release"
VERSION=$(<$ROOT_DIR/VERSION)
VERSION=$(jq -r ".version" $ROOT_DIR/package.json)
# mkdir -p $RELEASE_DIR
@ -13,4 +13,4 @@ VERSION=$(<$ROOT_DIR/VERSION)
# docker image save "humanconnection/${app}:latest" | gzip > "${RELEASE_DIR}/${app}.${VERSION}.tar.gz"
# done
ghr -soft "${VERSION}"
ghr -c "${VERSION}" "${VERSION}"

View File

@ -240,6 +240,13 @@ $size-height-xlarge: 60px;
$size-height-footer: 64px;
$size-tappable-square: 44px;
/**
* @tokens Size Width
* @presenter Spacing
*/
$size-width-paginate: 100px;
/**
* @tokens Size Avatar
* @presenter Spacing
@ -247,8 +254,7 @@ $size-tappable-square: 44px;
$size-avatar-small: 34px;
$size-avatar-base: 44px;
$size-avatar-large: 64px;
$size-avatar-x-large: 114px;
$size-avatar-large: 114px;
/**
* @tokens Size Buttons
@ -259,7 +265,7 @@ $size-avatar-x-large: 114px;
$size-button-small: 26px;
/**
* @tokens Size Buttons
* @tokens Size Icons
* @presenter Spacing
*/

View File

@ -175,3 +175,7 @@ hr {
overflow-wrap: break-word;
word-wrap: break-word;
}
.dropdown-arrow {
font-size: $font-size-xx-small;
}

View File

@ -1,68 +0,0 @@
import { mount } from '@vue/test-utils'
import Avatar from './Avatar.vue'
const localVue = global.localVue
describe('Avatar.vue', () => {
let propsData = {}
const Wrapper = () => {
return mount(Avatar, { propsData, localVue })
}
it('renders no image', () => {
expect(
Wrapper()
.find('img')
.exists(),
).toBe(false)
})
// this is testing the style guide
it('renders an icon', () => {
expect(
Wrapper()
.find('.ds-icon')
.exists(),
).toBe(true)
})
describe('given a user', () => {
describe('with a relative avatar url', () => {
beforeEach(() => {
propsData = {
user: {
avatar: '/avatar.jpg',
},
}
})
it('adds a prefix to load the image from the uploads service', () => {
expect(
Wrapper()
.find('img')
.attributes('src'),
).toBe('/api/avatar.jpg')
})
})
describe('with an absolute avatar url', () => {
beforeEach(() => {
propsData = {
user: {
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/sawalazar/128.jpg',
},
}
})
it('keeps the avatar URL as is', () => {
// e.g. our seeds have absolute image URLs
expect(
Wrapper()
.find('img')
.attributes('src'),
).toBe('https://s3.amazonaws.com/uifaces/faces/twitter/sawalazar/128.jpg')
})
})
})
})

View File

@ -1,28 +0,0 @@
<template>
<ds-avatar
:image="user && user.avatar | proxyApiUrl"
:name="userName"
class="avatar"
:size="size"
/>
</template>
<script>
export default {
name: 'HcAvatar',
props: {
user: { type: Object, default: null },
size: { type: String, default: 'small' },
},
computed: {
userName() {
const { name } = this.user || {}
// The name is used to display the initials in case
// the image cannot be loaded.
return name
// If the name is undefined, then our styleguide will
// display an icon for the anonymous user.
},
},
}
</script>

View File

@ -42,9 +42,9 @@ describe('AvatarMenu.vue', () => {
wrapper = Wrapper()
})
it('renders the HcAvatar component', () => {
it('renders the UserAvatar component', () => {
wrapper.find('.avatar-menu-trigger').trigger('click')
expect(wrapper.find('.ds-avatar').exists()).toBe(true)
expect(wrapper.find('.user-avatar').exists()).toBe(true)
})
describe('given a userName', () => {

View File

@ -11,7 +11,7 @@
"
@click.prevent="toggleMenu"
>
<hc-avatar :user="user" />
<user-avatar :user="user" />
<base-icon class="dropdown-arrow" name="angle-down" />
</a>
</template>
@ -49,12 +49,12 @@
<script>
import { mapGetters } from 'vuex'
import Dropdown from '~/components/Dropdown'
import HcAvatar from '~/components/Avatar/Avatar.vue'
import UserAvatar from '~/components/_new/generic/UserAvatar/UserAvatar'
export default {
components: {
Dropdown,
HcAvatar,
UserAvatar,
},
props: {
placement: { type: String, default: 'top-end' },

View File

@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils'
import CategoriesSelect from './CategoriesSelect'
import Vue from 'vue'
const localVue = global.localVue
@ -55,8 +56,9 @@ describe('CategoriesSelect.vue', () => {
})
describe('toggleCategory', () => {
beforeEach(() => {
beforeEach(async () => {
wrapper.vm.categories = categories
await Vue.nextTick()
democracyAndPolitics = wrapper.findAll('button').at(0)
democracyAndPolitics.trigger('click')
})

View File

@ -12,13 +12,13 @@
<div v-else :class="{ comment: true, 'disabled-content': comment.deleted || comment.disabled }">
<ds-card :id="anchor" :class="{ 'comment--target': isTarget }">
<ds-space margin-bottom="small" margin-top="small">
<hc-user :user="author" :date-time="comment.createdAt">
<user-teaser :user="author" :date-time="comment.createdAt">
<template v-slot:dateTime>
<ds-text v-if="comment.createdAt !== comment.updatedAt">
({{ $t('comment.edited') }})
</ds-text>
</template>
</hc-user>
</user-teaser>
<client-only>
<content-menu
v-show="!openEditCommentMenu"
@ -70,7 +70,7 @@
<script>
import { mapGetters } from 'vuex'
import { COMMENT_MAX_UNTRUNCATED_LENGTH, COMMENT_TRUNCATE_TO_LENGTH } from '~/constants/comment'
import HcUser from '~/components/User/User'
import UserTeaser from '~/components/UserTeaser/UserTeaser'
import ContentMenu from '~/components/ContentMenu/ContentMenu'
import ContentViewer from '~/components/Editor/ContentViewer'
import HcCommentForm from '~/components/CommentForm/CommentForm'
@ -92,7 +92,7 @@ export default {
}
},
components: {
HcUser,
UserTeaser,
ContentMenu,
ContentViewer,
HcCommentForm,

View File

@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils'
import CommentForm from './CommentForm'
import Vue from 'vue'
import MutationObserver from 'mutation-observer'
global.MutationObserver = MutationObserver
@ -74,6 +74,7 @@ describe('CommentForm.vue', () => {
it('calls `clear` method when the cancel button is clicked', async () => {
wrapper.vm.updateEditorContent('ok')
await Vue.nextTick()
await wrapper.find('[data-test="cancel-button"]').trigger('submit')
expect(cancelMethodSpy).toHaveBeenCalledTimes(1)
})

View File

@ -407,49 +407,49 @@ describe('ContentMenu.vue', () => {
).toBe('/settings')
})
it('can block other users', () => {
it('can mute other users', () => {
const wrapper = openContentMenu({
isOwner: false,
resourceType: 'user',
resource: {
id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
isBlocked: false,
isMuted: false,
},
})
wrapper
.findAll('.ds-menu-item')
.filter(item => item.text() === 'settings.blocked-users.block')
.filter(item => item.text() === 'settings.muted-users.mute')
.at(0)
.trigger('click')
expect(wrapper.emitted('block')).toEqual([
expect(wrapper.emitted('mute')).toEqual([
[
{
id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
isBlocked: false,
isMuted: false,
},
],
])
})
it('can unblock blocked users', () => {
it('can unmute muted users', () => {
const wrapper = openContentMenu({
isOwner: false,
resourceType: 'user',
resource: {
id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
isBlocked: true,
isMuted: true,
},
})
wrapper
.findAll('.ds-menu-item')
.filter(item => item.text() === 'settings.blocked-users.unblock')
.filter(item => item.text() === 'settings.muted-users.unmute')
.at(0)
.trigger('click')
expect(wrapper.emitted('unblock')).toEqual([
expect(wrapper.emitted('unmute')).toEqual([
[
{
id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
isBlocked: true,
isMuted: true,
},
],
])

View File

@ -155,19 +155,19 @@ export default {
icon: 'edit',
})
} else {
if (this.resource.isBlocked) {
if (this.resource.isMuted) {
routes.push({
label: this.$t(`settings.blocked-users.unblock`),
label: this.$t(`settings.muted-users.unmute`),
callback: () => {
this.$emit('unblock', this.resource)
this.$emit('unmute', this.resource)
},
icon: 'user-plus',
})
} else {
routes.push({
label: this.$t(`settings.blocked-users.block`),
label: this.$t(`settings.muted-users.mute`),
callback: () => {
this.$emit('block', this.resource)
this.$emit('mute', this.resource)
},
icon: 'user-times',
})

View File

@ -1,6 +1,7 @@
import { config, mount } from '@vue/test-utils'
import ContributionForm from './ContributionForm.vue'
import Vue from 'vue'
import Vuex from 'vuex'
import PostMutations from '~/graphql/PostMutations.js'
import CategoriesSelect from '~/components/CategoriesSelect/CategoriesSelect'
@ -147,31 +148,31 @@ describe('ContributionForm.vue', () => {
dataPrivacyButton.trigger('click')
})
it('title should not be empty', async () => {
it('title cannot be empty', async () => {
postTitleInput.setValue('')
wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
})
it('title should not be too long', async () => {
it('title cannot be too long', async () => {
postTitleInput.setValue(postTitleTooLong)
wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
})
it('title should not be too short', async () => {
it('title cannot be too short', async () => {
postTitleInput.setValue(postTitleTooShort)
wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
})
it('content should not be empty', async () => {
it('content cannot be empty', async () => {
await wrapper.vm.updateEditorContent('')
await wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
})
it('should have at least one category', async () => {
it('has at least one category', async () => {
dataPrivacyButton = await wrapper
.find(CategoriesSelect)
.find('[data-test="category-buttons-cat12"]')
@ -180,8 +181,9 @@ describe('ContributionForm.vue', () => {
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
})
it('should have not have more than three categories', async () => {
it('has no more than three categories', async () => {
wrapper.vm.form.categoryIds = ['cat4', 'cat9', 'cat15', 'cat27']
await Vue.nextTick()
wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
})
@ -209,10 +211,12 @@ describe('ContributionForm.vue', () => {
wrapper.find(CategoriesSelect).setData({ categories })
englishLanguage = wrapper.findAll('li').filter(language => language.text() === 'English')
englishLanguage.trigger('click')
await Vue.nextTick()
dataPrivacyButton = await wrapper
.find(CategoriesSelect)
.find('[data-test="category-buttons-cat12"]')
dataPrivacyButton.trigger('click')
await Vue.nextTick()
})
it('creates a post with valid title, content, and at least one category', async () => {
@ -278,10 +282,12 @@ describe('ContributionForm.vue', () => {
wrapper.find(CategoriesSelect).setData({ categories })
englishLanguage = wrapper.findAll('li').filter(language => language.text() === 'English')
englishLanguage.trigger('click')
await Vue.nextTick()
dataPrivacyButton = await wrapper
.find(CategoriesSelect)
.find('[data-test="category-buttons-cat12"]')
dataPrivacyButton.trigger('click')
await Vue.nextTick()
})
it('shows an error toaster when apollo mutation rejects', async () => {
@ -370,6 +376,7 @@ describe('ContributionForm.vue', () => {
it('supports updating categories', async () => {
expectedParams.variables.categoryIds.push('cat3')
wrapper.find(CategoriesSelect).setData({ categories })
await Vue.nextTick()
const healthWellbeingButton = await wrapper
.find(CategoriesSelect)
.find('[data-test="category-buttons-cat3"]')

View File

@ -38,7 +38,7 @@
<ds-space />
<client-only>
<hc-user :user="currentUser" :trunc="35" />
<user-teaser :user="currentUser" />
</client-only>
<ds-space />
<ds-input
@ -122,14 +122,14 @@ import locales from '~/locales'
import PostMutations from '~/graphql/PostMutations.js'
import HcCategoriesSelect from '~/components/CategoriesSelect/CategoriesSelect'
import HcTeaserImage from '~/components/TeaserImage/TeaserImage'
import HcUser from '~/components/User/User'
import UserTeaser from '~/components/UserTeaser/UserTeaser'
export default {
components: {
HcEditor,
HcCategoriesSelect,
HcTeaserImage,
HcUser,
UserTeaser,
},
props: {
contribution: { type: Object, default: () => {} },

View File

@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils'
import DeleteData from './DeleteData.vue'
import Vue from 'vue'
import Vuex from 'vuex'
const localVue = global.localVue
@ -81,7 +81,7 @@ describe('DeleteData.vue', () => {
})
it('does not call the delete user mutation if deleteEnabled is false', () => {
deleteAccountBtn = wrapper.find('.ds-button-danger')
deleteAccountBtn = wrapper.find('[data-test="delete-button"]')
deleteAccountBtn.trigger('click')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
})
@ -90,7 +90,7 @@ describe('DeleteData.vue', () => {
beforeEach(() => {
enableDeletionInput = wrapper.find('.enable-deletion-input input')
enableDeletionInput.setValue(deleteAccountName)
deleteAccountBtn = wrapper.find('.ds-button-danger')
deleteAccountBtn = wrapper.find('[data-test="delete-button"]')
})
it('if deleteEnabled is true and only deletes user by default', () => {
@ -168,7 +168,8 @@ describe('DeleteData.vue', () => {
it('shows an error toaster when the mutation rejects', async () => {
enableDeletionInput = wrapper.find('.enable-deletion-input input')
enableDeletionInput.setValue(deleteAccountName)
deleteAccountBtn = wrapper.find('.ds-button-danger')
await Vue.nextTick()
deleteAccountBtn = wrapper.find('[data-test="delete-button"]')
await deleteAccountBtn.trigger('click')
// second submission causes mutation to reject
await deleteAccountBtn.trigger('click')

View File

@ -55,22 +55,19 @@
<ds-space margin-bottom="xx-small" />
<ds-flex :gutter="{ base: 'xx-small', md: 'small', lg: 'large' }">
<ds-flex-item :width="{ base: '100%', sm: '100%', md: '100%', lg: 1.75 }">
<ds-input
v-model="enableDeletionValue"
@input="enableDeletion"
class="enable-deletion-input"
/>
<ds-input v-model="enableDeletionValue" class="enable-deletion-input" />
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', sm: '100%', md: '100%', lg: 1 }">
<ds-button
<base-button
icon="trash"
danger
filled
:disabled="!deleteEnabled"
data-test="delete-button"
@click="handleSubmit"
>
{{ $t('settings.deleteUserAccount.name') }}
</ds-button>
</base-button>
</ds-flex-item>
</ds-flex>
</ds-container>
@ -88,7 +85,6 @@ export default {
return {
deleteContributions: false,
deleteComments: false,
deleteEnabled: false,
enableDeletionValue: null,
}
},
@ -96,16 +92,14 @@ export default {
...mapGetters({
currentUser: 'auth/user',
}),
deleteEnabled() {
return this.enableDeletionValue === this.currentUser.name
},
},
methods: {
...mapActions({
logout: 'auth/logout',
}),
enableDeletion() {
if (this.enableDeletionValue === this.currentUser.name) {
this.deleteEnabled = true
}
},
handleSubmit() {
const resourceArgs = []
if (this.deleteContributions) {

View File

@ -65,10 +65,6 @@ export default {
}
}
.dropdown-arrow {
font-size: $font-size-xx-small;
}
.dropdown-menu {
user-select: none;
display: flex;

View File

@ -328,85 +328,4 @@ li > p {
margin: 0 0 $space-x-small;
}
}
.ProseMirror[contenteditable='false'] {
.embed-close-button {
display: none;
}
}
.embed-container {
position: relative;
padding: 0;
margin: $space-small auto;
overflow: hidden;
border-radius: $border-radius-base;
border: 1px solid $color-neutral-70;
background-color: $color-neutral-90;
}
.embed-content {
width: 100%;
height: 100%;
h4 {
margin: $space-small 0 0 $space-small;
}
p,
a {
display: block;
margin: 0 0 0 $space-small;
}
}
.embed-preview-image {
width: 100%;
height: auto;
max-height: 450px;
}
.embed-preview-image--clickable {
cursor: pointer;
}
.embed-html {
width: 100%;
iframe {
width: 100%;
}
}
.embed-overlay {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
padding: $space-large;
background-color: $color-neutral-100;
}
.embed-buttons {
button {
margin-right: $space-small;
}
}
.embed-checkbox {
display: flex;
input {
margin-right: $space-small;
}
}
.embed-close-button {
position: absolute;
top: $space-x-small;
right: $space-x-small;
background-color: rgba(250, 249, 250, 0.6);
}
</style>

View File

@ -65,13 +65,13 @@ describe('EmbedComponent.vue', () => {
})
it('shows the description', () => {
expect(wrapper.find('.embed-content p').text()).toBe(
expect(wrapper.find('.content p').text()).toBe(
'Salut tout le monde ! Aujourdhui, une vidéo sur le scepticisme, nous allons parler médiumnité avec le cas de Bruno CHARVET : « Bruno, un nouveau message ». Merci de rester respectueux dans les commentaires : SOURCES : Les sources des vi...',
)
})
it('shows preview Images for link', () => {
expect(wrapper.find('.embed-preview-image').exists()).toBe(true)
expect(wrapper.find('.preview').exists()).toBe(true)
})
})
@ -92,7 +92,7 @@ describe('EmbedComponent.vue', () => {
})
it('show the desciption', () => {
expect(wrapper.find('.embed-content p').text()).toBe(
expect(wrapper.find('.content p').text()).toBe(
'Shes incapable of controlling her limbs when her kitty is around. The obsession grows every day. Ps. Thats a sleep sack shes in. Not a starfish outfit. Al...',
)
})
@ -121,12 +121,12 @@ describe('EmbedComponent.vue', () => {
})
it('shows a simple link when a user closes the embed preview', () => {
wrapper.find('.embed-close-button').trigger('click')
wrapper.find('.close-button').trigger('click')
expect(wrapper.vm.showLinkOnly).toBe(true)
})
it('opens the data privacy overlay when a user clicks on the preview image', () => {
wrapper.find('.embed-preview-image--clickable').trigger('click')
wrapper.find('.preview.--clickable').trigger('click')
expect(wrapper.vm.showOverlay).toBe(true)
})
@ -135,19 +135,19 @@ describe('EmbedComponent.vue', () => {
wrapper.setData({ showOverlay: true })
})
it('when user agress', () => {
wrapper.find('.ds-button-primary').trigger('click')
it('when user agrees', () => {
wrapper.find('[data-test="play-now-button"]').trigger('click')
expect(wrapper.vm.showEmbed).toBe(true)
})
it('does not show iframe when user clicks to cancel', () => {
wrapper.find('.ds-button-ghost').trigger('click')
wrapper.find('[data-test="cancel-button"]').trigger('click')
expect(wrapper.vm.showEmbed).toBe(false)
})
describe("doesn't set permanently", () => {
beforeEach(() => {
wrapper.find('.ds-button-primary').trigger('click')
wrapper.find('[data-test="play-now-button"]').trigger('click')
})
it("if user doesn't give consent", () => {
@ -162,7 +162,7 @@ describe('EmbedComponent.vue', () => {
describe('sets permanently', () => {
beforeEach(() => {
wrapper.find('input[type=checkbox]').trigger('click')
wrapper.find('.ds-button-primary').trigger('click')
wrapper.find('[data-test="play-now-button"]').trigger('click')
})
it('changes setting permanetly when user requests', () => {
@ -194,7 +194,7 @@ describe('EmbedComponent.vue', () => {
})
it('does not display image to click', () => {
expect(wrapper.find('.embed-preview-image--clickable').exists()).toBe(false)
expect(wrapper.find('.preview.--clickable').exists()).toBe(false)
})
})
})

View File

@ -2,17 +2,17 @@
<a v-if="showLinkOnly" :href="dataEmbedUrl" rel="noopener noreferrer nofollow" target="_blank">
{{ dataEmbedUrl }}
</a>
<ds-container v-else width="small" class="embed-container">
<section class="embed-content">
<div v-if="showEmbed" v-html="embedHtml" class="embed-html" />
<ds-container v-else width="small" class="embed-component">
<section class="content">
<div v-if="showEmbed" v-html="embedHtml" class="html" />
<template v-else>
<img
v-if="embedHtml && embedImage"
:src="embedImage"
class="embed-preview-image embed-preview-image--clickable"
class="preview --clickable"
@click.prevent="openOverlay()"
/>
<img v-else-if="embedImage" :src="embedImage" class="embed-preview-image" />
<img v-else-if="embedImage" :src="embedImage" class="preview" />
</template>
<h4 v-if="embedTitle">{{ embedTitle }}</h4>
<p v-if="embedDescription">{{ embedDescription }}</p>
@ -20,25 +20,27 @@
{{ dataEmbedUrl }}
</a>
</section>
<aside v-if="showOverlay" class="embed-overlay">
<aside v-if="showOverlay" class="overlay">
<h3>{{ $t('editor.embed.data_privacy_warning') }}</h3>
<ds-text>{{ $t('editor.embed.data_privacy_info') }} {{ embedPublisher }}</ds-text>
<div class="embed-buttons">
<ds-button primary @click.prevent="allowEmbed()">
<div class="buttons">
<base-button primary @click="allowEmbed()" data-test="play-now-button">
{{ $t('editor.embed.play_now') }}
</ds-button>
<ds-button ghost @click.prevent="closeOverlay()">{{ $t('actions.cancel') }}</ds-button>
</base-button>
<base-button @click="closeOverlay()" data-test="cancel-button">
{{ $t('actions.cancel') }}
</base-button>
</div>
<label class="embed-checkbox">
<label class="checkbox">
<input type="checkbox" v-model="checkedAlwaysAllowEmbeds" />
<span>{{ $t('editor.embed.always_allow') }}</span>
</label>
</aside>
<ds-button
<base-button
icon="close"
ghost
size="small"
class="embed-close-button"
circle
class="close-button"
@click.prevent="removeEmbed()"
/>
</ds-container>
@ -151,3 +153,86 @@ export default {
},
}
</script>
<style lang="scss">
.embed-component {
position: relative;
padding: 0;
margin: $space-small auto;
overflow: hidden;
border-radius: $border-radius-base;
border: 1px solid $color-neutral-70;
background-color: $color-neutral-90;
> .content {
width: 100%;
height: 100%;
h4 {
margin: $space-small 0 0 $space-small;
}
p,
a {
display: block;
margin: 0 0 0 $space-small;
}
.html {
width: 100%;
iframe {
width: 100%;
}
}
.preview {
width: 100%;
height: auto;
max-height: 450px;
}
.preview.--clickable {
cursor: pointer;
}
}
> .overlay {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
padding: $space-large;
background-color: $color-neutral-100;
> .buttons {
.base-button {
margin-right: $space-small;
white-space: nowrap;
}
}
> .checkbox {
display: flex;
input {
margin-right: $space-small;
}
}
}
> .close-button {
position: absolute;
top: $space-x-small;
right: $space-x-small;
}
}
.ProseMirror[contenteditable='false'] {
.close-button {
display: none;
}
}
</style>

View File

@ -0,0 +1,61 @@
<template>
<div class="emotion-button">
<base-button :id="emotion" circle ghost @click="$emit('toggleEmotion', emotion)">
<img class="image" :src="emojiPath" />
</base-button>
<label class="label" :for="emotion">{{ $t(`contribution.emotions-label.${emotion}`) }}</label>
<p v-if="emotionCount !== null" class="count">{{ emotionCount }}x</p>
</div>
</template>
<script>
export default {
name: 'EmotionButton',
props: {
emojiPath: {
type: String,
required: true,
},
emotion: {
type: String,
required: true,
},
emotionCount: {
type: Number,
default: null,
},
},
}
</script>
<style lang="scss">
.emotion-button {
flex-grow: 1;
display: flex;
flex-direction: column;
align-items: center;
> .base-button {
padding: 0;
&:hover {
padding: $space-xxx-small;
}
}
> .label {
margin-top: $space-x-small;
font-size: $font-size-small;
cursor: pointer;
}
> .count {
margin: $space-x-small 0;
}
.image {
max-width: $size-button-base;
height: 100%;
}
}
</style>

View File

@ -1,27 +1,26 @@
<template>
<ds-flex :gutter="{ lg: 'large' }" class="emotions-flex">
<div v-for="emotion in Object.keys(PostsEmotionsCountByEmotion)" :key="emotion">
<ds-flex-item :width="{ lg: '100%' }">
<hc-emotions-button
@toggleEmotion="toggleEmotion"
:PostsEmotionsCountByEmotion="PostsEmotionsCountByEmotion"
:iconPath="iconPath(emotion)"
:emotion="emotion"
/>
</ds-flex-item>
</div>
</ds-flex>
<div class="emotions-button-group">
<emotion-button
v-for="emotion in Object.keys(PostsEmotionsCountByEmotion)"
:key="emotion"
:emojiPath="iconPath(emotion)"
:emotion="emotion"
:emotionCount="PostsEmotionsCountByEmotion[emotion]"
@toggleEmotion="toggleEmotion"
/>
</div>
</template>
<script>
import gql from 'graphql-tag'
import { mapGetters } from 'vuex'
import HcEmotionsButton from '~/components/EmotionsButton/EmotionsButton'
import EmotionButton from '~/components/EmotionButton/EmotionButton'
import { PostsEmotionsByCurrentUser } from '~/graphql/PostQuery.js'
import PostMutations from '~/graphql/PostMutations.js'
export default {
components: {
HcEmotionsButton,
EmotionButton,
},
props: {
post: { type: Object, default: () => {} },
@ -113,3 +112,9 @@ export default {
},
}
</script>
<style lang="scss">
.emotions-button-group {
display: flex;
}
</style>

View File

@ -1,49 +0,0 @@
<template>
<div>
<ds-button size="large" ghost @click="toggleEmotion(emotion)" class="emotions-buttons">
<img :src="iconPath" width="40" />
</ds-button>
<ds-space margin-bottom="xx-small" />
<div class="emotions-mobile-space">
<p class="emotions-label">{{ $t(`contribution.emotions-label.${emotion}`) }}</p>
<p style="display: inline" :key="PostsEmotionsCountByEmotion[emotion]">
{{ PostsEmotionsCountByEmotion[emotion] }}x
</p>
</div>
</div>
</template>
<script>
export default {
props: {
iconPath: { type: String, default: null },
PostsEmotionsCountByEmotion: { type: Object, default: () => {} },
emotion: { type: String, default: null },
},
methods: {
toggleEmotion(emotion) {
this.$emit('toggleEmotion', emotion)
},
},
}
</script>
<style lang="scss">
.emotions-flex {
justify-content: space-evenly;
text-align: center;
}
.emotions-label {
font-size: $font-size-small;
}
.emotions-buttons {
&:hover {
background-color: $background-color-base;
}
}
@media only screen and (max-width: 960px) {
.emotions-mobile-space {
margin-bottom: 32px;
}
}
</style>

View File

@ -150,7 +150,7 @@ describe('FilterPosts.vue', () => {
describe('click on an "emotions-buttons" button', () => {
it('calls TOGGLE_EMOTION when clicked', () => {
const wrapper = openFilterPosts()
happyEmotionButton = wrapper.findAll('button.emotions-buttons').at(1)
happyEmotionButton = wrapper.findAll('.emotion-button .base-button').at(1)
happyEmotionButton.trigger('click')
expect(mutations['posts/TOGGLE_EMOTION']).toHaveBeenCalledWith({}, 'happy')
})
@ -158,7 +158,7 @@ describe('FilterPosts.vue', () => {
it('sets the attribute `src` to colorized image', () => {
getters['posts/filteredByEmotions'] = jest.fn(() => ['happy'])
const wrapper = openFilterPosts()
happyEmotionButton = wrapper.findAll('button.emotions-buttons').at(1)
happyEmotionButton = wrapper.findAll('.emotion-button .base-button').at(1)
const happyEmotionButtonImage = happyEmotionButton.find('img')
expect(happyEmotionButtonImage.attributes().src).toEqual('/img/svg/emoji/happy_color.svg')
})

View File

@ -6,54 +6,42 @@
</ds-flex>
<ds-flex :gutter="{ lg: 'large' }">
<ds-flex-item
:width="{ base: '100%', sm: '100%', md: '100%', lg: '10%' }"
class="categories-menu-item"
:width="{ base: '100%', sm: '100%', md: '10%', lg: '10%' }"
class="follow-filter"
>
<ds-flex>
<ds-flex-item width="10%" />
<ds-space margin-bottom="xx-small" />
<ds-flex-item width="100%">
<div class="follow-filter-button">
<base-button
data-test="filter-by-followed"
icon="user-plus"
circle
:filled="filteredByUsersFollowed"
@click="toggleFilteredByFollowed(user.id)"
v-tooltip="{
content: this.$t('contribution.filterFollow'),
placement: 'left',
delay: { show: 500 },
}"
/>
<ds-space margin-bottom="x-small" />
<ds-flex-item>
<label class="follow-label">{{ $t('filter-posts.followers.label') }}</label>
</ds-flex-item>
<ds-space />
</div>
</ds-flex-item>
</ds-flex>
<base-button
data-test="filter-by-followed"
icon="user-plus"
circle
:filled="filteredByUsersFollowed"
@click="toggleFilteredByFollowed(user.id)"
v-tooltip="{
content: this.$t('contribution.filterFollow'),
placement: 'left',
delay: { show: 500 },
}"
/>
<label class="follow-label">{{ $t('filter-posts.followers.label') }}</label>
</ds-flex-item>
<div v-for="emotion in emotionsArray" :key="emotion">
<ds-flex-item :width="{ lg: '100%' }">
<base-button @click="toogleFilteredByEmotions(emotion)" class="emotions-buttons" circle>
<img :src="iconPath(emotion)" width="40" />
</base-button>
<ds-space margin-bottom="x-small" />
<ds-flex-item class="emotions-mobile-space text-center">
<label class="emotions-label">{{ $t(`contribution.emotions-label.${emotion}`) }}</label>
</ds-flex-item>
</ds-flex-item>
</div>
<emotion-button
v-for="emotion in emotionsArray"
:key="emotion"
:emojiPath="iconPath(emotion)"
:emotion="emotion"
@toggleEmotion="toogleFilteredByEmotions(emotion)"
/>
<ds-space margin-bottom="large" />
</ds-flex>
</ds-space>
</template>
<script>
import { mapGetters, mapMutations } from 'vuex'
import EmotionButton from '~/components/EmotionButton/EmotionButton'
export default {
components: {
EmotionButton,
},
props: {
user: { type: Object, required: true },
},
@ -91,13 +79,22 @@ export default {
display: block;
}
.follow-filter.ds-flex-item {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: $space-base;
> .follow-label {
margin-top: $space-x-small;
text-align: center;
}
}
@media only screen and (max-width: 960px) {
#filter-posts-header {
text-align: center;
}
.follow-filter-button {
float: left;
}
}
.text-center {

View File

@ -1,15 +0,0 @@
<template>
<ds-space class="load-more" margin-top="large" style="text-align: center">
<ds-button :loading="loading" icon="arrow-down" ghost @click="$emit('click')">
{{ $t('actions.loadMore') }}
</ds-button>
</ds-space>
</template>
<script>
export default {
props: {
loading: { type: Boolean, default: false },
},
}
</script>

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'
import Vue from 'vue'
import MasonryGrid from './MasonryGrid'
const localVue = global.localVue
@ -13,29 +13,29 @@ describe('MasonryGrid', () => {
masonryGridItem = wrapper.vm.$children[0]
})
it('adds the "reset-grid-height" class when itemsCalculating is more than 0', () => {
it('adds the "reset-grid-height" class when itemsCalculating is more than 0', async () => {
wrapper.setData({ itemsCalculating: 1 })
await Vue.nextTick()
expect(wrapper.classes()).toContain('reset-grid-height')
})
it('removes the "reset-grid-height" class when itemsCalculating is 0', () => {
it('removes the "reset-grid-height" class when itemsCalculating is 0', async () => {
wrapper.setData({ itemsCalculating: 0 })
await Vue.nextTick()
expect(wrapper.classes()).not.toContain('reset-grid-height')
})
it('adds 1 to itemsCalculating when a child emits "calculating-item-height"', () => {
it('adds 1 to itemsCalculating when a child emits "calculating-item-height"', async () => {
wrapper.setData({ itemsCalculating: 0 })
masonryGridItem.$emit('calculating-item-height')
await Vue.nextTick()
expect(wrapper.vm.itemsCalculating).toBe(1)
})
it('subtracts 1 from itemsCalculating when a child emits "finished-calculating-item-height"', () => {
it('subtracts 1 from itemsCalculating when a child emits "finished-calculating-item-height"', async () => {
wrapper.setData({ itemsCalculating: 2 })
masonryGridItem.$emit('finished-calculating-item-height')
await Vue.nextTick()
expect(wrapper.vm.itemsCalculating).toBe(1)
})
})

View File

@ -5,6 +5,7 @@ import DisableModal from './Modal/DisableModal.vue'
import ReportModal from './Modal/ReportModal.vue'
import Vuex from 'vuex'
import { getters, mutations } from '../store/modal'
import Vue from 'vue'
const localVue = global.localVue
@ -89,8 +90,9 @@ describe('Modal.vue', () => {
})
describe('child component emits close', () => {
it('turns empty', () => {
it('turns empty', async () => {
wrapper.find(DisableModal).vm.$emit('close')
await Vue.nextTick()
expect(wrapper.contains(DisableModal)).toBe(false)
})
})

View File

@ -1,5 +1,6 @@
import { config, shallowMount, mount } from '@vue/test-utils'
import ReportModal from './ReportModal.vue'
import Vue from 'vue'
const localVue = global.localVue
@ -151,9 +152,11 @@ describe('ReportModal.vue', () => {
})
describe('click confirm button', () => {
beforeEach(() => {
beforeEach(async () => {
wrapper.find('.ds-radio-option-label').trigger('click')
await Vue.nextTick()
wrapper.find('button.confirm').trigger('click')
await Vue.nextTick()
})
it('calls report mutation', () => {

View File

@ -2,7 +2,7 @@
<ds-space :class="{ read: notification.read, notification: true }" margin-bottom="x-small">
<client-only>
<ds-space margin-bottom="x-small">
<hc-user :user="from.author" :date-time="from.createdAt" :trunc="35" />
<user-teaser :user="from.author" :date-time="from.createdAt" />
</ds-space>
<ds-text class="reason-text-for-test" color="soft">
{{ $t(`notifications.reason.${notification.reason}`) }}
@ -35,12 +35,12 @@
</template>
<script>
import HcUser from '~/components/User/User'
import UserTeaser from '~/components/UserTeaser/UserTeaser'
export default {
name: 'Notification',
components: {
HcUser,
UserTeaser,
},
props: {
notification: {

View File

@ -89,8 +89,8 @@ describe('NotificationsTable.vue', () => {
})
it('renders the author', () => {
const username = firstRowNotification.find('.username')
expect(username.text()).toEqual(postNotification.from.author.name)
const userinfo = firstRowNotification.find('.user-teaser > .info')
expect(userinfo.text()).toContain(postNotification.from.author.name)
})
it('renders the reason for the notification', () => {
@ -122,8 +122,8 @@ describe('NotificationsTable.vue', () => {
})
it('renders the author', () => {
const username = secondRowNotification.find('.username')
expect(username.text()).toEqual(commentNotification.from.author.name)
const userinfo = secondRowNotification.find('.user-teaser > .info')
expect(userinfo.text()).toContain(commentNotification.from.author.name)
})
it('renders the reason for the notification', () => {

View File

@ -4,7 +4,7 @@ import { action } from '@storybook/addon-actions'
import NotificationsTable from '~/components/NotificationsTable/NotificationsTable'
import helpers from '~/storybook/helpers'
import { post } from '~/components/PostCard/PostCard.story.js'
import { user } from '~/components/User/User.story.js'
import { user } from '~/components/UserTeaser/UserTeaser.story.js'
helpers.init()
export const notifications = [

View File

@ -15,10 +15,9 @@
<template #user="scope">
<ds-space margin-bottom="base">
<client-only>
<hc-user
<user-teaser
:user="scope.row.from.author"
:date-time="scope.row.from.createdAt"
:trunc="35"
:class="{ 'notification-status': scope.row.read }"
/>
</client-only>
@ -50,12 +49,12 @@
<hc-empty v-else icon="alert" :message="$t('notifications.empty')" />
</template>
<script>
import HcUser from '~/components/User/User'
import UserTeaser from '~/components/UserTeaser/UserTeaser'
import HcEmpty from '~/components/Empty/Empty'
export default {
components: {
HcUser,
UserTeaser,
HcEmpty,
},
props: {

View File

@ -18,12 +18,23 @@
{{ $t('site.faq') }}
</a>
<span>-</span>
<a href="https://github.com/Human-Connection/Human-Connection/releases" target="_blank">
{{ $t('site.changelog') }}
<a
href="https://github.com/Human-Connection/Human-Connection/blob/master/CHANGELOG.md"
target="_blank"
>
{{ version }}
</a>
</div>
</template>
<script>
export default {
data() {
return { version: `v${process.env.release}` }
},
}
</script>
<style lang="scss" scoped>
.ds-footer {
text-align: center;

View File

@ -1,26 +0,0 @@
<template>
<ds-flex direction="row-reverse">
<ds-flex-item width="50px">
<ds-button @click="next" :disabled="!hasNext" icon="arrow-right" primary />
</ds-flex-item>
<ds-flex-item width="50px">
<ds-button @click="back" :disabled="!hasPrevious" icon="arrow-left" primary />
</ds-flex-item>
</ds-flex>
</template>
<script>
export default {
props: {
hasNext: { type: Boolean, default: false },
hasPrevious: { type: Boolean, default: false },
},
methods: {
back() {
this.$emit('back')
},
next() {
this.$emit('next')
},
},
}
</script>

View File

@ -20,14 +20,14 @@
<!-- Username, Image & Date of Post -->
<div class="user-wrapper">
<client-only>
<hc-user :user="post.author" :trunc="35" :date-time="post.createdAt" />
<user-teaser :user="post.author" :date-time="post.createdAt" />
</client-only>
<hc-ribbon v-if="isPinned" class="ribbon--pinned" :text="$t('post.pinned')" />
<hc-ribbon v-else :text="$t('post.name')" />
</div>
<ds-space margin-bottom="small" />
<!-- Post Title -->
<ds-heading tag="h3" no-margin class="hyphenate-text">{{ post.title }}</ds-heading>
<ds-heading tag="h3" class="hyphenate-text post-title">{{ post.title }}</ds-heading>
<ds-space margin-bottom="small" />
<!-- Post Content Excerpt -->
<!-- eslint-disable vue/no-v-html -->
@ -78,7 +78,7 @@
</template>
<script>
import HcUser from '~/components/User/User'
import UserTeaser from '~/components/UserTeaser/UserTeaser'
import ContentMenu from '~/components/ContentMenu/ContentMenu'
import HcCategory from '~/components/Category'
import HcRibbon from '~/components/Ribbon'
@ -89,7 +89,7 @@ import { postMenuModalsData, deletePostMutation } from '~/components/utils/PostH
export default {
name: 'HcPostCard',
components: {
HcUser,
UserTeaser,
HcCategory,
HcRibbon,
ContentMenu,
@ -186,7 +186,11 @@ export default {
height: 75px;
}
/* workaround to avoid jumping layout when hc-user is rendered */
.post-title {
margin-top: $space-large;
}
/* workaround to avoid jumping layout when user-teaser is rendered */
.user-wrapper {
height: 36px;
}

View File

@ -1,8 +1,8 @@
import { config, mount } from '@vue/test-utils'
import Vue from 'vue'
import { VERSION } from '~/constants/terms-and-conditions-version.js'
import CreateUserAccount from './CreateUserAccount'
import { SignupVerificationMutation } from '~/graphql/Registration.js'
const localVue = global.localVue
config.stubs['sweetalert-icon'] = '<span><slot /></span>'
@ -110,6 +110,7 @@ describe('CreateUserAccount', () => {
it('displays success', async () => {
await action()
await Vue.nextTick()
expect(mocks.$t).toHaveBeenCalledWith(
'components.registration.create-user-account.success',
)
@ -140,6 +141,7 @@ describe('CreateUserAccount', () => {
it('displays form errors', async () => {
await action()
await Vue.nextTick()
expect(mocks.$t).toHaveBeenCalledWith(
'components.registration.create-user-account.error',
)

View File

@ -70,14 +70,6 @@ export default {
showCropper: false,
}
},
watch: {
error() {
const that = this
setTimeout(function() {
that.error = false
}, 2000)
},
},
methods: {
template() {
return `<div class="dz-preview dz-file-preview">
@ -90,6 +82,9 @@ export default {
verror(file, message) {
this.error = true
this.$toast.error(file.status, message)
setTimeout(() => {
this.error = false
}, 2000)
},
transformImage(file) {
this.file = file

View File

@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils'
import Vue from 'vue'
import Upload from '.'
const localVue = global.localVue
@ -57,8 +58,9 @@ describe('Upload', () => {
expect(mocks.$toast.error).toHaveBeenCalledWith(fileError.status, message)
})
it('changes error status from false to true to false', () => {
it('changes error status from false to true to false', async () => {
wrapper.vm.verror(fileError, message)
await Vue.nextTick()
expect(wrapper.vm.error).toEqual(true)
jest.runAllTimers()
expect(wrapper.vm.error).toEqual(false)

View File

@ -1,5 +1,5 @@
import { mount, RouterLinkStub } from '@vue/test-utils'
import User from './User.vue'
import UserTeaser from './UserTeaser.vue'
import Vuex from 'vuex'
const localVue = global.localVue
@ -7,7 +7,7 @@ const filter = jest.fn(str => str)
localVue.filter('truncate', filter)
describe('User', () => {
describe('UserTeaser', () => {
let propsData
let mocks
let stubs
@ -35,7 +35,7 @@ describe('User', () => {
const store = new Vuex.Store({
getters,
})
return mount(User, { store, propsData, mocks, stubs, localVue })
return mount(UserTeaser, { store, propsData, mocks, stubs, localVue })
}
it('renders anonymous user', () => {

View File

@ -1,6 +1,6 @@
import { storiesOf } from '@storybook/vue'
import { withA11y } from '@storybook/addon-a11y'
import User from '~/components/User/User.vue'
import UserTeaser from '~/components/UserTeaser/UserTeaser.vue'
import helpers from '~/storybook/helpers'
helpers.init()
@ -48,30 +48,38 @@ export const user = {
],
followedByCount: 0,
followedByCurrentUser: false,
isBlocked: false,
isMuted: false,
followedBy: [],
socialMedia: [],
}
storiesOf('User', module)
storiesOf('UserTeaser', module)
.addDecorator(withA11y)
.addDecorator(helpers.layout)
.add('available', () => ({
components: { User },
.add('user only', () => ({
components: { UserTeaser },
store: helpers.store,
data: () => ({
user,
}),
template: '<user :user="user" :trunc="35" :date-time="new Date()" />',
template: '<user-teaser :user="user" />',
}))
.add('with Date', () => ({
components: { UserTeaser },
store: helpers.store,
data: () => ({
user,
}),
template: '<user-teaser :user="user" :date-time="new Date()" />',
}))
.add('has edited something', () => ({
components: { User },
components: { UserTeaser },
store: helpers.store,
data: () => ({
user,
}),
template: `
<user :user="user" :trunc="35" :date-time="new Date()">
<user-teaser :user="user" :date-time="new Date()">
<template v-slot:dateTime>
- HEY! I'm edited
</template>
@ -79,10 +87,10 @@ storiesOf('User', module)
`,
}))
.add('anonymous', () => ({
components: { User },
components: { UserTeaser },
store: helpers.store,
data: () => ({
user: null,
}),
template: '<user :user="user" :trunc="35" :date-time="new Date()" />',
template: '<user-teaser :user="user" :date-time="new Date()" />',
}))

View File

@ -1,32 +1,37 @@
<template>
<div class="user" v-if="displayAnonymous">
<hc-avatar v-if="showAvatar" class="avatar" />
<div>
<b class="username">{{ $t('profile.userAnonym') }}</b>
</div>
<div class="user-teaser" v-if="displayAnonymous">
<user-avatar v-if="showAvatar" />
<span class="info anonymous">{{ $t('profile.userAnonym') }}</span>
</div>
<dropdown v-else :class="{ 'disabled-content': user.disabled }" placement="top-start" offset="0">
<template slot="default" slot-scope="{ openMenu, closeMenu, isOpen }">
<nuxt-link :to="userLink" :class="['user', isOpen && 'active']">
<div @mouseover="showPopover ? openMenu(true) : () => {}" @mouseleave="closeMenu(true)">
<hc-avatar v-if="showAvatar" class="avatar" :user="user" />
<div>
<ds-text class="userinfo">
<b>{{ userSlug }}</b>
</ds-text>
</div>
<ds-text class="username" align="left" size="small" color="soft">
{{ userName | truncate(18) }}
<template v-if="dateTime">
<base-icon name="clock" />
<hc-relative-date-time :date-time="dateTime" />
<slot name="dateTime"></slot>
</template>
</ds-text>
<dropdown
v-else
:class="[{ 'disabled-content': user.disabled }]"
placement="top-start"
offset="0"
>
<template #default="{ openMenu, closeMenu, isOpen }">
<nuxt-link
:to="userLink"
:class="['user-teaser', isOpen && 'active']"
@mouseover.native="showPopover ? openMenu(true) : () => {}"
@mouseleave.native="closeMenu(true)"
>
<user-avatar v-if="showAvatar" :user="user" size="small" />
<div class="info">
<span class="text">
<span class="slug">{{ userSlug }}</span>
<span v-if="dateTime">{{ userName }}</span>
</span>
<span v-if="dateTime" class="text">
<base-icon name="clock" />
<hc-relative-date-time :date-time="dateTime" />
<slot name="dateTime"></slot>
</span>
<span v-else class="text">{{ userName }}</span>
</div>
</nuxt-link>
</template>
<template slot="popover" v-if="showPopover">
<template #popover v-if="showPopover">
<div style="min-width: 250px">
<hc-badges v-if="user.badges && user.badges.length" :badges="user.badges" />
<ds-text
@ -77,7 +82,6 @@
/>
</ds-flex-item>
</ds-flex>
<!--<ds-space margin-bottom="x-small" />-->
</div>
</template>
</dropdown>
@ -89,22 +93,21 @@ import { mapGetters } from 'vuex'
import HcRelativeDateTime from '~/components/RelativeDateTime'
import HcFollowButton from '~/components/FollowButton'
import HcBadges from '~/components/Badges'
import HcAvatar from '~/components/Avatar/Avatar.vue'
import UserAvatar from '~/components/_new/generic/UserAvatar/UserAvatar'
import Dropdown from '~/components/Dropdown'
export default {
name: 'HcUser',
name: 'UserTeaser',
components: {
HcRelativeDateTime,
HcFollowButton,
HcAvatar,
UserAvatar,
HcBadges,
Dropdown,
},
props: {
user: { type: Object, default: null },
showAvatar: { type: Boolean, default: true },
trunc: { type: Number, default: 18 }, // "-1" is no trunc
dateTime: { type: [Date, String], default: null },
showPopover: { type: Boolean, default: true },
},
@ -147,38 +150,51 @@ export default {
}
</script>
<style scoped lang="scss">
.avatar {
float: left;
margin-right: 4px;
height: 100%;
vertical-align: middle;
<style lang="scss">
.trigger {
max-width: 100%;
}
.userinfo {
.user-teaser {
display: flex;
align-items: center;
> .ds-text {
display: flex;
align-items: center;
margin-left: $space-xx-small;
}
}
.user {
white-space: nowrap;
flex-wrap: nowrap;
z-index: $z-index-post-card-link;
position: relative;
display: flex;
align-items: center;
&:hover,
&.active {
z-index: 999;
> .user-avatar {
flex-shrink: 0;
}
}
.user-slug {
margin-bottom: $space-xx-small;
> .info {
display: flex;
flex-direction: column;
justify-content: center;
padding-left: $space-xx-small;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: $text-color-soft;
font-size: $font-size-small;
&.anonymous {
font-size: $font-size-base;
}
.slug {
color: $color-primary;
font-size: $font-size-base;
}
}
.text {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
> .ds-text {
display: inline;
}
}
}
</style>

View File

@ -1,39 +1,35 @@
import { mount } from '@vue/test-utils'
import Paginate from './Paginate'
import PaginationButtons from './PaginationButtons'
const localVue = global.localVue
describe('Paginate.vue', () => {
let propsData, wrapper, nextButton, backButton
beforeEach(() => {
propsData = {}
})
describe('PaginationButtons.vue', () => {
let propsData = {}
let wrapper
let nextButton
let backButton
const Wrapper = () => {
return mount(Paginate, { propsData, localVue })
return mount(PaginationButtons, { propsData, localVue })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
describe('mount', () => {
describe('next button', () => {
beforeEach(() => {
propsData.hasNext = true
wrapper = Wrapper()
nextButton = wrapper.findAll('.ds-button').at(0)
nextButton = wrapper.find('[data-test="next-button"]')
})
it('is disabled by default', () => {
propsData = {}
wrapper = Wrapper()
nextButton = wrapper.findAll('.ds-button').at(0)
nextButton = wrapper.find('[data-test="next-button"]')
expect(nextButton.attributes().disabled).toEqual('disabled')
})
it('is not disabled if hasNext is true', () => {
it('is enabled if hasNext is true', () => {
expect(nextButton.attributes().disabled).toBeUndefined()
})
@ -47,17 +43,17 @@ describe('Paginate.vue', () => {
beforeEach(() => {
propsData.hasPrevious = true
wrapper = Wrapper()
backButton = wrapper.findAll('.ds-button').at(1)
backButton = wrapper.find('[data-test="previous-button"]')
})
it('is disabled by default', () => {
propsData = {}
wrapper = Wrapper()
backButton = wrapper.findAll('.ds-button').at(1)
backButton = wrapper.find('[data-test="previous-button"]')
expect(backButton.attributes().disabled).toEqual('disabled')
})
it('is not disabled if hasPrevious is true', () => {
it('is enabled if hasPrevious is true', () => {
expect(backButton.attributes().disabled).toBeUndefined()
})

View File

@ -1,16 +1,16 @@
import { storiesOf } from '@storybook/vue'
import { withA11y } from '@storybook/addon-a11y'
import { action } from '@storybook/addon-actions'
import Paginate from '~/components/Paginate/Paginate'
import PaginationButtons from '~/components/_new/generic/PaginationButtons/PaginationButtons'
import helpers from '~/storybook/helpers'
helpers.init()
storiesOf('Paginate', module)
storiesOf('PaginationButtons', module)
.addDecorator(withA11y)
.addDecorator(helpers.layout)
.add('basic pagination', () => ({
components: { Paginate },
components: { PaginationButtons },
data: () => ({
hasNext: true,
hasPrevious: false,
@ -19,7 +19,7 @@ storiesOf('Paginate', module)
back: action('back'),
next: action('next'),
},
template: `<paginate
template: `<pagination-buttons
:hasNext="hasNext"
:hasPrevious="hasPrevious"
@back="back"

View File

@ -0,0 +1,42 @@
<template>
<div class="pagination-buttons">
<base-button
@click="$emit('back')"
:disabled="!hasPrevious"
icon="arrow-left"
circle
data-test="previous-button"
/>
<base-button
@click="$emit('next')"
:disabled="!hasNext"
icon="arrow-right"
circle
data-test="next-button"
/>
</div>
</template>
<script>
export default {
props: {
hasNext: {
type: Boolean,
default: false,
},
hasPrevious: {
type: Boolean,
default: false,
},
},
}
</script>
<style lang="scss">
.pagination-buttons {
display: flex;
justify-content: space-around;
width: $size-width-paginate;
margin: $space-x-small auto;
}
</style>

View File

@ -0,0 +1,99 @@
import { mount } from '@vue/test-utils'
import UserAvatar from './UserAvatar.vue'
import BaseIcon from '~/components/_new/generic/BaseIcon/BaseIcon'
const localVue = global.localVue
describe('UserAvatar.vue', () => {
let propsData, wrapper
beforeEach(() => {
propsData = {}
wrapper = Wrapper()
})
const Wrapper = () => {
return mount(UserAvatar, { propsData, localVue })
}
it('renders no image', () => {
expect(wrapper.find('img').exists()).toBe(false)
})
it('renders an icon', () => {
expect(wrapper.find(BaseIcon).exists()).toBe(true)
})
describe('given a user', () => {
describe('with no image', () => {
beforeEach(() => {
propsData = {
user: {
name: 'Matt Rider',
},
}
wrapper = Wrapper()
})
describe('no user name', () => {
it('renders an icon', () => {
propsData = { user: { name: null } }
wrapper = Wrapper()
expect(wrapper.find(BaseIcon).exists()).toBe(true)
})
})
describe("user name is 'Anonymous'", () => {
it('renders an icon', () => {
propsData = { user: { name: 'Anonymous' } }
wrapper = Wrapper()
expect(wrapper.find(BaseIcon).exists()).toBe(true)
})
})
it('displays user initials', () => {
expect(wrapper.find('.initials').text()).toEqual('MR')
})
it('displays no more than 3 initials', () => {
propsData = { user: { name: 'Ana Paula Nunes Marques' } }
wrapper = Wrapper()
expect(wrapper.find('.initials').text()).toEqual('APN')
})
})
describe('with a relative avatar url', () => {
beforeEach(() => {
propsData = {
user: {
name: 'Not Anonymous',
avatar: '/avatar.jpg',
},
}
wrapper = Wrapper()
})
it('adds a prefix to load the image from the uploads service', () => {
expect(wrapper.find('.image').attributes('src')).toBe('/api/avatar.jpg')
})
})
describe('with an absolute avatar url', () => {
beforeEach(() => {
propsData = {
user: {
name: 'Not Anonymous',
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/sawalazar/128.jpg',
},
}
wrapper = Wrapper()
})
it('keeps the avatar URL as is', () => {
// e.g. our seeds have absolute image URLs
expect(wrapper.find('.image').attributes('src')).toBe(
'https://s3.amazonaws.com/uifaces/faces/twitter/sawalazar/128.jpg',
)
})
})
})
})

View File

@ -0,0 +1,57 @@
import { storiesOf } from '@storybook/vue'
import { withA11y } from '@storybook/addon-a11y'
import StoryRouter from 'storybook-vue-router'
import UserAvatar from '~/components/_new/generic/UserAvatar/UserAvatar'
import helpers from '~/storybook/helpers'
import { user } from '~/components/UserTeaser/UserTeaser.story.js'
helpers.init()
const anonymousUser = {
...user,
name: 'Anonymous',
avatar: null,
}
const userWithoutAvatar = {
...user,
avatar: null,
name: 'Ana Paula Nunes Marques',
}
storiesOf('UserAvatar', module)
.addDecorator(withA11y)
.addDecorator(helpers.layout)
.addDecorator(StoryRouter())
.add('with image', () => ({
components: { UserAvatar },
data: () => ({
user,
}),
template: '<user-avatar :user="user" />',
}))
.add('without image, anonymous user', () => ({
components: { UserAvatar },
data: () => ({
user: anonymousUser,
}),
template: '<user-avatar :user="user" />',
}))
.add('without image, user initials', () => ({
components: { UserAvatar },
data: () => ({
user: userWithoutAvatar,
}),
template: '<user-avatar :user="user" />',
}))
.add('small', () => ({
components: { UserAvatar },
data: () => ({
user,
}),
template: '<user-avatar :user="user" size="small"/>',
}))
.add('large', () => ({
components: { UserAvatar },
data: () => ({
user,
}),
template: '<user-avatar :user="user" size="large"/>',
}))

View File

@ -0,0 +1,84 @@
<template>
<div :class="['user-avatar', size && `--${this.size}`]">
<span class="initials">{{ userInitials }}</span>
<base-icon v-if="isAnonymous" name="eye-slash" />
<img
v-else
:src="user.avatar | proxyApiUrl"
class="image"
@error="event.target.style.display = 'none'"
/>
</div>
</template>
<script>
export default {
name: 'UserAvatar',
props: {
size: {
type: String,
required: false,
validator: value => {
return value.match(/(small|large)/)
},
},
user: {
type: Object,
default: null,
},
},
computed: {
isAnonymous() {
return !this.user || !this.user.name || this.user.name.toLowerCase() === 'anonymous'
},
userInitials() {
if (this.isAnonymous) return ''
return this.user.name
.match(/\b\w/g)
.join('')
.substring(0, 3)
.toUpperCase()
},
},
}
</script>
<style lang="scss">
.user-avatar {
position: relative;
height: $size-avatar-base;
width: $size-avatar-base;
border-radius: 50%;
overflow: hidden;
background-color: $color-primary-dark;
color: $text-color-primary-inverse;
&.--small {
width: $size-avatar-small;
height: $size-avatar-small;
}
&.--large {
width: $size-avatar-large;
height: $size-avatar-large;
font-size: $font-size-xx-large;
}
> .initials,
> .base-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
> .image {
position: relative;
z-index: 5;
width: 100%;
object-fit: cover;
object-position: center;
}
}
</style>

View File

@ -7,11 +7,10 @@
condensed
>
<template #submitter="scope">
<hc-user
<user-teaser
:user="scope.row.submitter"
:showAvatar="false"
:showPopover="false"
:trunc="30"
data-test="filing-user"
/>
</template>
@ -29,12 +28,12 @@
</ds-table>
</template>
<script>
import HcUser from '~/components/User/User'
import UserTeaser from '~/components/UserTeaser/UserTeaser'
import HcRelativeDateTime from '~/components/RelativeDateTime'
export default {
components: {
HcUser,
UserTeaser,
HcRelativeDateTime,
},
props: {

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