Merge branch 'master' of github.com:Human-Connection/Human-Connection into 1724-block-users

This commit is contained in:
mattwr18 2020-01-21 21:15:03 +01:00
commit 704b8c2d57
202 changed files with 8142 additions and 4566 deletions

1
.github/stale.yml vendored
View File

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

View File

@ -6,7 +6,6 @@ addons:
- libgconf-2-4 - libgconf-2-4
snaps: snaps:
- docker - docker
- chromium
install: install:
- yarn global add wait-on - 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,10 +4,219 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [v0.2.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
- 🍰 Search For Users [`#2262`](https://github.com/Human-Connection/Human-Connection/pull/2262)
- Use node LTS in production [`#2713`](https://github.com/Human-Connection/Human-Connection/pull/2713)
- build(deps): bump apollo-server from 2.9.15 to 2.9.16 in /backend [`#2718`](https://github.com/Human-Connection/Human-Connection/pull/2718)
- build(deps): bump neo4j-graphql-js from 2.11.4 to 2.11.5 in /backend [`#2715`](https://github.com/Human-Connection/Human-Connection/pull/2715)
- build(deps-dev): bump apollo-server-testing from 2.9.15 to 2.9.16 in /backend [`#2720`](https://github.com/Human-Connection/Human-Connection/pull/2720)
- build(deps): bump @hapi/joi from 17.0.0 to 17.0.2 in /backend [`#2719`](https://github.com/Human-Connection/Human-Connection/pull/2719)
- build(deps): bump apollo-server-express from 2.9.15 to 2.9.16 in /backend [`#2717`](https://github.com/Human-Connection/Human-Connection/pull/2717)
- build(deps): bump metascraper-url from 5.8.13 to 5.9.5 in /backend [`#2716`](https://github.com/Human-Connection/Human-Connection/pull/2716)
- build(deps): bump date-fns from 2.8.1 to 2.9.0 in /backend [`#2706`](https://github.com/Human-Connection/Human-Connection/pull/2706)
- build(deps): bump metascraper-lang from 5.8.13 to 5.9.5 in /backend [`#2703`](https://github.com/Human-Connection/Human-Connection/pull/2703)
- build(deps): bump date-fns from 2.8.1 to 2.9.0 in /webapp [`#2711`](https://github.com/Human-Connection/Human-Connection/pull/2711)
- build(deps): bump metascraper-logo from 5.8.13 to 5.9.5 in /backend [`#2697`](https://github.com/Human-Connection/Human-Connection/pull/2697)
- build(deps): bump metascraper-title from 5.8.13 to 5.9.5 in /backend [`#2694`](https://github.com/Human-Connection/Human-Connection/pull/2694)
- build(deps): bump metascraper-description from 5.8.15 to 5.9.5 in /backend [`#2690`](https://github.com/Human-Connection/Human-Connection/pull/2690)
- build(deps): bump node from 13.5.0-alpine to 13.6.0-alpine in /webapp [`#2708`](https://github.com/Human-Connection/Human-Connection/pull/2708)
- build(deps): bump @sentry/node from 5.10.2 to 5.11.0 in /backend [`#2709`](https://github.com/Human-Connection/Human-Connection/pull/2709)
- build(deps): bump metascraper-audio from 5.8.13 to 5.9.5 in /backend [`#2707`](https://github.com/Human-Connection/Human-Connection/pull/2707)
- build(deps): bump metascraper-image from 5.9.4 to 5.9.5 in /backend [`#2705`](https://github.com/Human-Connection/Human-Connection/pull/2705)
- build(deps): bump metascraper-youtube from 5.8.13 to 5.9.5 in /backend [`#2704`](https://github.com/Human-Connection/Human-Connection/pull/2704)
- build(deps-dev): bump date-fns from 2.8.1 to 2.9.0 [`#2702`](https://github.com/Human-Connection/Human-Connection/pull/2702)
- build(deps): bump metascraper-soundcloud from 5.9.0 to 5.9.5 in /backend [`#2693`](https://github.com/Human-Connection/Human-Connection/pull/2693)
- build(deps): bump metascraper-date from 5.8.13 to 5.9.5 in /backend [`#2698`](https://github.com/Human-Connection/Human-Connection/pull/2698)
- build(deps): bump neo4j-graphql-js from 2.11.3 to 2.11.4 in /backend [`#2696`](https://github.com/Human-Connection/Human-Connection/pull/2696)
- build(deps): bump metascraper-video from 5.8.13 to 5.9.5 in /backend [`#2695`](https://github.com/Human-Connection/Human-Connection/pull/2695)
- build(deps): bump metascraper-publisher from 5.8.13 to 5.9.5 in /backend [`#2692`](https://github.com/Human-Connection/Human-Connection/pull/2692)
- build(deps): bump metascraper-author from 5.8.13 to 5.9.5 in /backend [`#2691`](https://github.com/Human-Connection/Human-Connection/pull/2691)
- build(deps): bump metascraper from 5.9.4 to 5.9.5 in /backend [`#2689`](https://github.com/Human-Connection/Human-Connection/pull/2689)
- Changes Text For SignUp [`#2678`](https://github.com/Human-Connection/Human-Connection/pull/2678)
- Update de.json [`#2655`](https://github.com/Human-Connection/Human-Connection/pull/2655)
- build(deps): bump neode from 0.3.6 to 0.3.7 in /backend [`#2682`](https://github.com/Human-Connection/Human-Connection/pull/2682)
- Update neo4j-driver [`#2546`](https://github.com/Human-Connection/Human-Connection/pull/2546)
- build(deps): bump merge-graphql-schemas from 1.7.5 to 1.7.6 in /backend [`#2681`](https://github.com/Human-Connection/Human-Connection/pull/2681)
- build(deps): bump neo4j-graphql-js from 2.11.2 to 2.11.3 in /backend [`#2680`](https://github.com/Human-Connection/Human-Connection/pull/2680)
- build(deps-dev): bump neode from 0.3.6 to 0.3.7 [`#2679`](https://github.com/Human-Connection/Human-Connection/pull/2679)
- Parse xss before extracting mentions/hashtags [`#2674`](https://github.com/Human-Connection/Human-Connection/pull/2674)
- build(deps): bump metascraper-logo from 5.8.12 to 5.8.13 in /backend [`#2672`](https://github.com/Human-Connection/Human-Connection/pull/2672)
- build(deps): bump metascraper from 5.9.0 to 5.9.4 in /backend [`#2668`](https://github.com/Human-Connection/Human-Connection/pull/2668)
- build(deps-dev): bump eslint-plugin-jest from 23.2.0 to 23.3.0 in /webapp [`#2671`](https://github.com/Human-Connection/Human-Connection/pull/2671)
- build(deps-dev): bump css-loader from 3.4.0 to 3.4.1 in /webapp [`#2669`](https://github.com/Human-Connection/Human-Connection/pull/2669)
- build(deps): bump metascraper-image from 5.8.13 to 5.9.4 in /backend [`#2670`](https://github.com/Human-Connection/Human-Connection/pull/2670)
- build(deps-dev): bump apollo-server-testing from 2.9.14 to 2.9.15 in /backend [`#2667`](https://github.com/Human-Connection/Human-Connection/pull/2667)
- build(deps): bump neo4j-graphql-js from 2.11.0 to 2.11.2 in /backend [`#2666`](https://github.com/Human-Connection/Human-Connection/pull/2666)
- build(deps): bump metascraper-title from 5.8.12 to 5.8.13 in /backend [`#2665`](https://github.com/Human-Connection/Human-Connection/pull/2665)
- build(deps): bump @hapi/joi from 16.1.8 to 17.0.0 in /backend [`#2664`](https://github.com/Human-Connection/Human-Connection/pull/2664)
- build(deps-dev): bump cypress-file-upload from 3.5.1 to 3.5.3 [`#2663`](https://github.com/Human-Connection/Human-Connection/pull/2663)
- build(deps-dev): bump eslint-plugin-jest from 23.1.1 to 23.3.0 in /backend [`#2662`](https://github.com/Human-Connection/Human-Connection/pull/2662)
- build(deps-dev): bump eslint-config-prettier from 6.7.0 to 6.9.0 in /webapp [`#2632`](https://github.com/Human-Connection/Human-Connection/pull/2632)
- build(deps-dev): bump slug from 2.0.0 to 2.1.0 [`#2647`](https://github.com/Human-Connection/Human-Connection/pull/2647)
- build(deps): bump merge-graphql-schemas from 1.7.3 to 1.7.5 in /backend [`#2648`](https://github.com/Human-Connection/Human-Connection/pull/2648)
- build(deps): bump metascraper-url from 5.8.12 to 5.8.13 in /backend [`#2637`](https://github.com/Human-Connection/Human-Connection/pull/2637)
- build(deps): bump metascraper-publisher from 5.8.12 to 5.8.13 in /backend [`#2636`](https://github.com/Human-Connection/Human-Connection/pull/2636)
- build(deps-dev): bump eslint-plugin-jest from 23.1.1 to 23.2.0 in /webapp [`#2642`](https://github.com/Human-Connection/Human-Connection/pull/2642)
- build(deps): bump graphql-shield from 7.0.5 to 7.0.7 in /backend [`#2649`](https://github.com/Human-Connection/Human-Connection/pull/2649)
- build(deps-dev): bump eslint-plugin-node from 10.0.0 to 11.0.0 in /backend [`#2650`](https://github.com/Human-Connection/Human-Connection/pull/2650)
- build(deps-dev): bump eslint-config-prettier from 6.7.0 to 6.9.0 in /backend [`#2651`](https://github.com/Human-Connection/Human-Connection/pull/2651)
- build(deps): bump slug from 2.0.0 to 2.1.0 in /backend [`#2652`](https://github.com/Human-Connection/Human-Connection/pull/2652)
- build(deps): bump metascraper from 5.8.12 to 5.9.0 in /backend [`#2654`](https://github.com/Human-Connection/Human-Connection/pull/2654)
- build(deps): bump metascraper-description from 5.8.12 to 5.8.15 in /backend [`#2653`](https://github.com/Human-Connection/Human-Connection/pull/2653)
- build(deps): bump metascraper-author from 5.8.12 to 5.8.13 in /backend [`#2616`](https://github.com/Human-Connection/Human-Connection/pull/2616)
- build(deps): bump metascraper-lang from 5.8.12 to 5.8.13 in /backend [`#2618`](https://github.com/Human-Connection/Human-Connection/pull/2618)
- build(deps): bump apollo-server from 2.9.13 to 2.9.15 in /backend [`#2634`](https://github.com/Human-Connection/Human-Connection/pull/2634)
- build(deps): bump metascraper-soundcloud from 5.8.15 to 5.9.0 in /backend [`#2638`](https://github.com/Human-Connection/Human-Connection/pull/2638)
- build(deps): bump metascraper-video from 5.8.12 to 5.8.13 in /backend [`#2639`](https://github.com/Human-Connection/Human-Connection/pull/2639)
- build(deps): bump mustache from 3.2.0 to 3.2.1 in /backend [`#2640`](https://github.com/Human-Connection/Human-Connection/pull/2640)
- build(deps): bump slug from 1.1.0 to 2.0.0 in /backend [`#2641`](https://github.com/Human-Connection/Human-Connection/pull/2641)
- build(deps-dev): bump @vue/cli-shared-utils from 4.1.1 to 4.1.2 in /webapp [`#2643`](https://github.com/Human-Connection/Human-Connection/pull/2643)
- build(deps-dev): bump eslint-plugin-vue from 6.1.1 to 6.1.2 in /webapp [`#2644`](https://github.com/Human-Connection/Human-Connection/pull/2644)
- build(deps-dev): bump eslint-plugin-node from 10.0.0 to 11.0.0 in /webapp [`#2645`](https://github.com/Human-Connection/Human-Connection/pull/2645)
- build(deps-dev): bump @babel/preset-env from 7.7.6 to 7.7.7 in /webapp [`#2579`](https://github.com/Human-Connection/Human-Connection/pull/2579)
- build(deps): bump metascraper-soundcloud from 5.8.12 to 5.8.15 in /backend [`#2630`](https://github.com/Human-Connection/Human-Connection/pull/2630)
- build(deps-dev): bump eslint from 6.7.2 to 6.8.0 in /webapp [`#2617`](https://github.com/Human-Connection/Human-Connection/pull/2617)
- build(deps): bump metascraper-date from 5.8.12 to 5.8.13 in /backend [`#2615`](https://github.com/Human-Connection/Human-Connection/pull/2615)
- build(deps): bump metascraper-image from 5.8.12 to 5.8.13 in /backend [`#2614`](https://github.com/Human-Connection/Human-Connection/pull/2614)
- build(deps): bump metascraper-youtube from 5.8.12 to 5.8.13 in /backend [`#2612`](https://github.com/Human-Connection/Human-Connection/pull/2612)
- build(deps): bump metascraper-audio from 5.8.12 to 5.8.13 in /backend [`#2610`](https://github.com/Human-Connection/Human-Connection/pull/2610)
- 🍰 Added Language Tag For Posts [`#2627`](https://github.com/Human-Connection/Human-Connection/pull/2627)
- build(deps-dev): bump cypress-plugin-retries from 1.5.0 to 1.5.2 [`#2609`](https://github.com/Human-Connection/Human-Connection/pull/2609)
- build(deps-dev): bump eslint from 6.7.2 to 6.8.0 in /backend [`#2613`](https://github.com/Human-Connection/Human-Connection/pull/2613)
- remove accidently created ru.json in wrong place [`#2606`](https://github.com/Human-Connection/Human-Connection/pull/2606)
- build(deps): bump neo4j from 3.5.13-enterprise to 3.5.14-enterprise in /neo4j [`#2620`](https://github.com/Human-Connection/Human-Connection/pull/2620)
- Fixes 2603 [`#2619`](https://github.com/Human-Connection/Human-Connection/pull/2619)
- build(deps-dev): bump @babel/core from 7.7.5 to 7.7.7 in /webapp [`#2581`](https://github.com/Human-Connection/Human-Connection/pull/2581)
- build(deps-dev): bump slug from 1.1.0 to 2.0.0 [`#2621`](https://github.com/Human-Connection/Human-Connection/pull/2621)
- build(deps): [security] bump handlebars from 4.1.2 to 4.5.3 in /webapp [`#2624`](https://github.com/Human-Connection/Human-Connection/pull/2624)
- build(deps): [security] bump handlebars from 4.1.2 to 4.5.3 in /backend [`#2625`](https://github.com/Human-Connection/Human-Connection/pull/2625)
- build(deps-dev): bump cypress from 3.8.0 to 3.8.1 [`#2626`](https://github.com/Human-Connection/Human-Connection/pull/2626)
- build(deps-dev): bump eslint-plugin-vue from 6.0.1 to 6.1.1 in /webapp [`#2633`](https://github.com/Human-Connection/Human-Connection/pull/2633)
- build(deps-dev): bump @babel/register from 7.7.4 to 7.7.7 [`#2571`](https://github.com/Human-Connection/Human-Connection/pull/2571)
- build(deps): bump neo4j-graphql-js from 2.10.2 to 2.11.0 in /backend [`#2600`](https://github.com/Human-Connection/Human-Connection/pull/2600)
- build(deps-dev): bump @babel/core from 7.7.5 to 7.7.7 in /backend [`#2590`](https://github.com/Human-Connection/Human-Connection/pull/2590)
- build(deps): bump metascraper-url from 5.8.7 to 5.8.12 in /backend [`#2599`](https://github.com/Human-Connection/Human-Connection/pull/2599)
- build(deps): bump metascraper-lang from 5.8.10 to 5.8.12 in /backend [`#2598`](https://github.com/Human-Connection/Human-Connection/pull/2598)
- build(deps): bump metascraper-audio from 5.8.10 to 5.8.12 in /backend [`#2596`](https://github.com/Human-Connection/Human-Connection/pull/2596)
- build(deps): bump node from 13.4.0-alpine to 13.5.0-alpine in /webapp [`#2595`](https://github.com/Human-Connection/Human-Connection/pull/2595)
- build(deps-dev): bump storybook-design-token from 0.4.1 to 0.5.0 in /webapp [`#2594`](https://github.com/Human-Connection/Human-Connection/pull/2594)
- build(deps): bump graphql-shield from 7.0.4 to 7.0.5 in /backend [`#2593`](https://github.com/Human-Connection/Human-Connection/pull/2593)
- build(deps): bump metascraper-publisher from 5.8.7 to 5.8.12 in /backend [`#2592`](https://github.com/Human-Connection/Human-Connection/pull/2592)
- build(deps-dev): bump @babel/preset-env from 7.7.6 to 7.7.7 in /backend [`#2568`](https://github.com/Human-Connection/Human-Connection/pull/2568)
- Fix imageAspectRatio set to null UpdatePost [`#2588`](https://github.com/Human-Connection/Human-Connection/pull/2588)
- 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)
#### [v0.2.0](https://github.com/Human-Connection/Human-Connection/compare/v0.1.13...v0.2.0) #### [v0.2.0](https://github.com/Human-Connection/Human-Connection/compare/v0.1.13...v0.2.0)
> 19 December 2019 > 19 December 2019
- 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): 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/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) - 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)
@ -62,8 +271,8 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Fix: User.name is not non-nullable [`#2510`](https://github.com/Human-Connection/Human-Connection/pull/2510) - Fix: User.name is not non-nullable [`#2510`](https://github.com/Human-Connection/Human-Connection/pull/2510)
- Update to version 0.1.13 [`#2506`](https://github.com/Human-Connection/Human-Connection/pull/2506) - Update to version 0.1.13 [`#2506`](https://github.com/Human-Connection/Human-Connection/pull/2506)
- Lokalise: update of webapp/locales/ru.json [`b70ff73`](https://github.com/Human-Connection/Human-Connection/commit/b70ff73bba98d28494c55ed12161288b1efa1516) - Lokalise: update of webapp/locales/ru.json [`b70ff73`](https://github.com/Human-Connection/Human-Connection/commit/b70ff73bba98d28494c55ed12161288b1efa1516)
- build(deps): bump apollo-server-express in /backend [`69d3107`](https://github.com/Human-Connection/Human-Connection/commit/69d3107cbcce8225dd14f7231936a597fba6105d) - Separate concerns in components [`d74d207`](https://github.com/Human-Connection/Human-Connection/commit/d74d2072ba41af6170d79d7dc2e24f9ebab15771)
- refactor: content menu [`71b2eac`](https://github.com/Human-Connection/Human-Connection/commit/71b2eac175e9d6e1a2bbba123490f281b7cb13f3) - Fix failing component tests [`b79c292`](https://github.com/Human-Connection/Human-Connection/commit/b79c292ef4f76b307131566c24d849c2e35c8089)
#### [v0.1.13](https://github.com/Human-Connection/Human-Connection/compare/v0.1.12...v0.1.13) #### [v0.1.13](https://github.com/Human-Connection/Human-Connection/compare/v0.1.12...v0.1.13)
@ -91,10 +300,10 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- build(deps): bump cookie-universal-nuxt from 2.0.19 to 2.1.0 in /webapp [`#2490`](https://github.com/Human-Connection/Human-Connection/pull/2490) - build(deps): bump cookie-universal-nuxt from 2.0.19 to 2.1.0 in /webapp [`#2490`](https://github.com/Human-Connection/Human-Connection/pull/2490)
- Update to version 0.1.12 [`#2483`](https://github.com/Human-Connection/Human-Connection/pull/2483) - Update to version 0.1.12 [`#2483`](https://github.com/Human-Connection/Human-Connection/pull/2483)
- Lokalise: update of locale/ru.json [`60b3035`](https://github.com/Human-Connection/Human-Connection/commit/60b3035a3d475cb481130c6fe94f2901711a4053) - Lokalise: update of locale/ru.json [`60b3035`](https://github.com/Human-Connection/Human-Connection/commit/60b3035a3d475cb481130c6fe94f2901711a4053)
- Fix search by adding result id [`ebc5cf3`](https://github.com/Human-Connection/Human-Connection/commit/ebc5cf392d92acf3a9e22c8967d02ea2cf6fd7fb)
- Write test/refactor tests/resolvers/middleware [`d375ebe`](https://github.com/Human-Connection/Human-Connection/commit/d375ebe7d90e3251b17f59ffba8fb1470923ebe8) - Write test/refactor tests/resolvers/middleware [`d375ebe`](https://github.com/Human-Connection/Human-Connection/commit/d375ebe7d90e3251b17f59ffba8fb1470923ebe8)
- refactor css, fix design issues [`5586335`](https://github.com/Human-Connection/Human-Connection/commit/5586335ed2b3474498e87b929f54d52562e44636)
#### [v0.1.12](https://github.com/Human-Connection/Human-Connection/compare/v0.1.10...v0.1.12) #### [v0.1.12](https://github.com/Human-Connection/Human-Connection/compare/v0.1.11...v0.1.12)
> 10 December 2019 > 10 December 2019
@ -204,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) - 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) - 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) - 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 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 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) - 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)
@ -253,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) - 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) - 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) - 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) - Fix #2294 [`#2294`](https://github.com/Human-Connection/Human-Connection/issues/2294)
- Merge pull request #2078 from Human-Connection/fix-2042-back-link [`#2042`](https://github.com/Human-Connection/Human-Connection/issues/2042) - Merge pull request #2078 from Human-Connection/fix-2042-back-link [`#2042`](https://github.com/Human-Connection/Human-Connection/issues/2042)
- Move components to components/features [`2357028`](https://github.com/Human-Connection/Human-Connection/commit/235702867d97b44dac37f8059f9194e23ba7f47d) - Tell github-linguists to ignore snapshots [`978347b`](https://github.com/Human-Connection/Human-Connection/commit/978347ba7b5a6aa1bc915ada972ffffa2816d37c)
- Add missing unit tests/refactor code [`b364065`](https://github.com/Human-Connection/Human-Connection/commit/b3640659bb608cc34edc6f2aca350f07dd2b9ce6) - Lokalise: update of webapp/locales/ru.json [`906e851`](https://github.com/Human-Connection/Human-Connection/commit/906e8518bf060134150187fb1574ac50ffd502f6)
- Add stories/specs for ReportList [`a59e72d`](https://github.com/Human-Connection/Human-Connection/commit/a59e72d8a8f491cb251e3e5acddea3b32144209b) - 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) #### [v0.1.10](https://github.com/Human-Connection/Human-Connection/compare/v0.1.9...v0.1.10)
@ -319,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 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) - 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) - 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 current file [`26c0d4d`](https://github.com/Human-Connection/Human-Connection/commit/26c0d4d83e4418a2378e05b66b6b47461f82735f)
- Add notifications page with Notifications in table [`7cdc12f`](https://github.com/Human-Connection/Human-Connection/commit/7cdc12f4b9943062e15a874dd39f8a50142b6c61) - 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) #### [v0.1.9](https://github.com/Human-Connection/Human-Connection/compare/v0.1.8...v0.1.9)
@ -388,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) - 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) - 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) - 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) - fix: Don't attempt to save locale if not authenticated [`#2025`](https://github.com/Human-Connection/Human-Connection/pull/2025)
@ -406,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 @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) - 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.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) - 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) - 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 > 23 October 2019
@ -426,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) - 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) - 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 > 22 October 2019
@ -460,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) - 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) - 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 > 17 October 2019
@ -518,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) - 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) - 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 > 10 October 2019
@ -556,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) - 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) - 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 > 4 October 2019
@ -576,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) - 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) - 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 > 2 October 2019
@ -635,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) - 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) - 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 > 27 September 2019
@ -709,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) - 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) - zwischenspeichern [`e4c7c11`](https://github.com/Human-Connection/Human-Connection/commit/e4c7c1125da6f8fa259241b4d3838b1e7b1e24a2)
#### 0.1.0 #### v0.1.0
> 18 September 2019 > 18 September 2019
@ -1847,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) - 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) - 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) - 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) - Remove package-lock.json [`25bd96e`](https://github.com/Human-Connection/Human-Connection/commit/25bd96eedf6be5b7ea6e94c8433d044e13d62e70)
- Copy package.json from webapp/ [`f3a9996`](https://github.com/Human-Connection/Human-Connection/commit/f3a9996962e5dd8b2e365a032c1a5766fe666103) - Remove Styleguide [`53ea934`](https://github.com/Human-Connection/Human-Connection/commit/53ea93492dcc7f861743cd50a4ddf7728c9d659b)

View File

@ -1 +0,0 @@
0.2.0

View File

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

View File

@ -1,13 +1,13 @@
{ {
"name": "human-connection-backend", "name": "human-connection-backend",
"version": "0.0.1", "version": "0.2.2",
"description": "GraphQL Backend for Human Connection", "description": "GraphQL Backend for Human Connection",
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {
"build": "babel src/ -d dist/ --copy-files", "build": "babel src/ -d dist/ --copy-files",
"start": "node dist/", "start": "node dist/",
"dev": "nodemon --exec babel-node src/ -e js,gql", "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", "lint": "eslint src --config .eslintrc.js",
"test": "jest --forceExit --detectOpenHandles --runInBand", "test": "jest --forceExit --detectOpenHandles --runInBand",
"db:reset": "babel-node src/seed/reset-db.js", "db:reset": "babel-node src/seed/reset-db.js",
@ -32,20 +32,20 @@
] ]
}, },
"dependencies": { "dependencies": {
"@hapi/joi": "^17.0.0", "@hapi/joi": "^17.1.0",
"@sentry/node": "^5.11.0", "@sentry/node": "^5.11.1",
"apollo-cache-inmemory": "~1.6.5", "apollo-cache-inmemory": "~1.6.5",
"apollo-client": "~2.6.8", "apollo-client": "~2.6.8",
"apollo-link-context": "~1.0.19", "apollo-link-context": "~1.0.19",
"apollo-link-http": "~1.5.16", "apollo-link-http": "~1.5.16",
"apollo-server": "~2.9.15", "apollo-server": "~2.9.16",
"apollo-server-express": "^2.9.14", "apollo-server-express": "^2.9.16",
"babel-plugin-transform-runtime": "^6.23.0", "babel-plugin-transform-runtime": "^6.23.0",
"bcryptjs": "~2.4.3", "bcryptjs": "~2.4.3",
"cheerio": "~1.0.0-rc.3", "cheerio": "~1.0.0-rc.3",
"cors": "~2.8.5", "cors": "~2.8.5",
"cross-env": "~6.0.3", "cross-env": "~6.0.3",
"date-fns": "2.8.1", "date-fns": "2.9.0",
"debug": "~4.1.1", "debug": "~4.1.1",
"dotenv": "~8.2.0", "dotenv": "~8.2.0",
"express": "^4.17.1", "express": "^4.17.1",
@ -62,48 +62,48 @@
"linkifyjs": "~2.1.8", "linkifyjs": "~2.1.8",
"lodash": "~4.17.14", "lodash": "~4.17.14",
"merge-graphql-schemas": "^1.7.6", "merge-graphql-schemas": "^1.7.6",
"metascraper": "^5.9.5", "metascraper": "^5.10.5",
"metascraper-audio": "^5.9.5", "metascraper-audio": "^5.10.5",
"metascraper-author": "^5.9.5", "metascraper-author": "^5.10.5",
"metascraper-clearbit-logo": "^5.3.0", "metascraper-clearbit-logo": "^5.3.0",
"metascraper-date": "^5.9.5", "metascraper-date": "^5.10.5",
"metascraper-description": "^5.9.5", "metascraper-description": "^5.10.5",
"metascraper-image": "^5.9.5", "metascraper-image": "^5.10.5",
"metascraper-lang": "^5.8.13", "metascraper-lang": "^5.10.5",
"metascraper-lang-detector": "^4.10.2", "metascraper-lang-detector": "^4.10.2",
"metascraper-logo": "^5.9.5", "metascraper-logo": "^5.10.5",
"metascraper-publisher": "^5.9.5", "metascraper-publisher": "^5.10.5",
"metascraper-soundcloud": "^5.9.5", "metascraper-soundcloud": "^5.10.5",
"metascraper-title": "^5.9.5", "metascraper-title": "^5.10.5",
"metascraper-url": "^5.8.13", "metascraper-url": "^5.10.5",
"metascraper-video": "^5.9.5", "metascraper-video": "^5.10.5",
"metascraper-youtube": "^5.9.5", "metascraper-youtube": "^5.10.5",
"minimatch": "^3.0.4", "minimatch": "^3.0.4",
"mustache": "^3.2.1", "mustache": "^4.0.0",
"neo4j-driver": "^4.0.1", "neo4j-driver": "^4.0.1",
"neo4j-graphql-js": "^2.11.4", "neo4j-graphql-js": "^2.11.5",
"neode": "^0.3.7", "neode": "^0.3.7",
"node-fetch": "~2.6.0", "node-fetch": "~2.6.0",
"nodemailer": "^6.4.2", "nodemailer": "^6.4.2",
"nodemailer-html-to-text": "^3.1.0", "nodemailer-html-to-text": "^3.1.0",
"npm-run-all": "~4.1.5", "npm-run-all": "~4.1.5",
"request": "~2.88.0", "request": "~2.88.0",
"sanitize-html": "~1.20.1", "sanitize-html": "~1.21.1",
"slug": "~2.1.0", "slug": "~2.1.0",
"trunc-html": "~1.1.2", "trunc-html": "~1.1.2",
"uuid": "~3.3.3", "uuid": "~3.4.0",
"validator": "^12.1.0", "validator": "^12.1.0",
"wait-on": "~3.3.0", "wait-on": "~4.0.0",
"xregexp": "^4.2.4" "xregexp": "^4.2.4"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "~7.7.7", "@babel/cli": "~7.8.3",
"@babel/core": "~7.7.7", "@babel/core": "~7.8.3",
"@babel/node": "~7.7.7", "@babel/node": "~7.8.3",
"@babel/plugin-proposal-throw-expressions": "^7.7.4", "@babel/plugin-proposal-throw-expressions": "^7.8.3",
"@babel/preset-env": "~7.7.7", "@babel/preset-env": "~7.8.3",
"@babel/register": "~7.7.0", "@babel/register": "~7.8.3",
"apollo-server-testing": "~2.9.15", "apollo-server-testing": "~2.9.16",
"babel-core": "~7.0.0-0", "babel-core": "~7.0.0-0",
"babel-eslint": "~10.0.3", "babel-eslint": "~10.0.3",
"babel-jest": "~24.9.0", "babel-jest": "~24.9.0",
@ -112,8 +112,8 @@
"eslint": "~6.8.0", "eslint": "~6.8.0",
"eslint-config-prettier": "~6.9.0", "eslint-config-prettier": "~6.9.0",
"eslint-config-standard": "~14.1.0", "eslint-config-standard": "~14.1.0",
"eslint-plugin-import": "~2.19.1", "eslint-plugin-import": "~2.20.0",
"eslint-plugin-jest": "~23.3.0", "eslint-plugin-jest": "~23.6.0",
"eslint-plugin-node": "~11.0.0", "eslint-plugin-node": "~11.0.0",
"eslint-plugin-prettier": "~3.1.2", "eslint-plugin-prettier": "~3.1.2",
"eslint-plugin-promise": "~4.2.1", "eslint-plugin-promise": "~4.2.1",

View File

@ -1,7 +1,8 @@
import dotenv from 'dotenv' import dotenv from 'dotenv'
import path from 'path' if (require.resolve) {
// are we in a nodejs environment?
dotenv.config({ path: path.resolve(__dirname, '../../.env') }) dotenv.config({ path: require.resolve('../../.env') })
}
const { const {
MAPBOX_TOKEN, MAPBOX_TOKEN,
@ -27,6 +28,15 @@ export const requiredConfigs = {
PRIVATE_KEY_PASSPHRASE, 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 = { export const smtpConfigs = {
SMTP_HOST, SMTP_HOST,
SMTP_PORT, SMTP_PORT,

View File

@ -85,6 +85,8 @@ export default shield(
Query: { Query: {
'*': deny, '*': deny,
findPosts: allow, findPosts: allow,
findUsers: allow,
findResources: allow,
embed: allow, embed: allow,
Category: allow, Category: allow,
Tag: allow, Tag: allow,
@ -99,6 +101,7 @@ export default shield(
Badge: allow, Badge: allow,
PostsEmotionsCountByEmotion: allow, PostsEmotionsCountByEmotion: allow,
PostsEmotionsByCurrentUser: isAuthenticated, PostsEmotionsByCurrentUser: isAuthenticated,
mutedUsers: isAuthenticated,
blockedUsers: isAuthenticated, blockedUsers: isAuthenticated,
notifications: isAuthenticated, notifications: isAuthenticated,
Donations: isAuthenticated, Donations: isAuthenticated,
@ -135,8 +138,10 @@ export default shield(
resetPassword: allow, resetPassword: allow,
AddPostEmotions: isAuthenticated, AddPostEmotions: isAuthenticated,
RemovePostEmotions: isAuthenticated, RemovePostEmotions: isAuthenticated,
block: isAuthenticated, muteUser: isAuthenticated,
unblock: isAuthenticated, unmuteUser: isAuthenticated,
blockUser: isAuthenticated,
unblockUser: isAuthenticated,
markAsRead: isAuthenticated, markAsRead: isAuthenticated,
AddEmailAddress: isAuthenticated, AddEmailAddress: isAuthenticated,
VerifyEmailAddress: isAuthenticated, VerifyEmailAddress: isAuthenticated,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
module.exports = { export default {
id: { type: 'string', primary: true }, id: { type: 'string', primary: true },
deleted: { type: 'boolean', default: false }, deleted: { type: 'boolean', default: false },
disabled: { 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 }, email: { type: 'string', primary: true, lowercase: true, email: true },
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
nonce: { type: 'string', token: true }, nonce: { type: 'string', token: true },

View File

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

View File

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

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

View File

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

View File

@ -8,42 +8,26 @@ import createOrUpdateLocations from './users/location'
const neode = getNeode() const neode = getNeode()
export const getBlockedUsers = async context => { export const getMutedUsers = async context => {
const { neode } = context const { neode } = context
const userModel = neode.model('User') const userModel = neode.model('User')
let blockedUsers = neode let mutedUsers = neode
.query() .query()
.match('user', userModel) .match('user', userModel)
.where('user.id', context.user.id) .where('user.id', context.user.id)
.relationship(userModel.relationships().get('blocked')) .relationship(userModel.relationships().get('muted'))
.to('blocked', userModel) .to('muted', userModel)
.return('blocked') .return('muted')
blockedUsers = await blockedUsers.execute() mutedUsers = await mutedUsers.execute()
blockedUsers = blockedUsers.records.map(r => r.get('blocked').properties) mutedUsers = mutedUsers.records.map(r => r.get('muted').properties)
return blockedUsers return mutedUsers
}
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
} }
export default { export default {
Query: { Query: {
blockedUsers: async (object, args, context, resolveInfo) => { mutedUsers: async (object, args, context, resolveInfo) => {
try { try {
return getBlockedUsers(context) return getMutedUsers(context)
} catch (e) { } catch (e) {
throw new UserInputError(e.message) throw new UserInputError(e.message)
} }
@ -72,7 +56,37 @@ export default {
}, },
}, },
Mutation: { Mutation: {
block: async (object, args, context, resolveInfo) => { 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()
},
blockUser: async (object, args, context, resolveInfo) => {
const { user: currentUser } = context const { user: currentUser } = context
if (currentUser.id === args.id) return null if (currentUser.id === args.id) return null
await neode.cypher( await neode.cypher(
@ -89,7 +103,7 @@ export default {
await user.relateTo(blockedUser, 'blocked') await user.relateTo(blockedUser, 'blocked')
return blockedUser.toJson() return blockedUser.toJson()
}, },
unblock: async (object, args, context, resolveInfo) => { unblockUser: async (object, args, context, resolveInfo) => {
const { user: currentUser } = context const { user: currentUser } = context
if (currentUser.id === args.id) return null if (currentUser.id === args.id) return null
await neode.cypher( await neode.cypher(
@ -217,6 +231,8 @@ export default {
'MATCH (this)<-[:FOLLOWS]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1', 'MATCH (this)<-[:FOLLOWS]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
isBlocked: isBlocked:
'MATCH (this)<-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1', 'MATCH (this)<-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
isMuted:
'MATCH (this)<-[:MUTED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
}, },
count: { count: {
contributionsCount: contributionsCount:

View File

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

View File

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

View File

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

View File

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

View File

@ -76,6 +76,13 @@ type User {
""" """
) )
isMuted: Boolean! @cypher(
statement: """
MATCH (this)<-[:MUTED]-(user:User { id: $cypherParams.currentUserId})
RETURN COUNT(user) >= 1
"""
)
# contributions: [WrittenPost]! # contributions: [WrittenPost]!
# contributions2(first: Int = 10, offset: Int = 0): [WrittenPost2]! # contributions2(first: Int = 10, offset: Int = 0): [WrittenPost2]!
# @cypher( # @cypher(
@ -160,8 +167,23 @@ type Query {
orderBy: [_UserOrdering] orderBy: [_UserOrdering]
filter: _UserFilter filter: _UserFilter
): [User] ): [User]
mutedUsers: [User]
blockedUsers: [User] blockedUsers: [User]
isLoggedIn: Boolean!
currentUser: User currentUser: User
findUsers(query: String!,limit: Int = 10, filter: _UserFilter): [User]!
@cypher(
statement: """
CALL db.index.fulltext.queryNodes('user_fulltext_search', $query)
YIELD node as post, score
MATCH (user)
WHERE score >= 0.2
AND NOT user.deleted = true AND NOT user.disabled = true
RETURN user
LIMIT $limit
"""
)
} }
type Mutation { type Mutation {
@ -179,13 +201,13 @@ type Mutation {
termsAndConditionsAgreedAt: String termsAndConditionsAgreedAt: String
allowEmbedIframes: Boolean allowEmbedIframes: Boolean
showShoutsPublicly: Boolean showShoutsPublicly: Boolean
locale: String locale: String
): User ): User
DeleteUser(id: ID!, resource: [Deletable]): User DeleteUser(id: ID!, resource: [Deletable]): User
muteUser(id: ID!): User
block(id: ID!): User unmuteUser(id: ID!): User
unblock(id: ID!): User blockUser(id: ID!): User
unblockUser(id: ID!): User
} }

View File

@ -1,30 +1,18 @@
import { getDriver, getNeode } from '../../bootstrap/neo4j' 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 = { const factories = {
Badge: createBadge, Badge: require('./badges.js').default,
User: createUser, User: require('./users.js').default,
Post: createPost, Post: require('./posts.js').default,
Comment: createComment, Comment: require('./comments.js').default,
Category: createCategory, Category: require('./categories.js').default,
Tag: createTag, Tag: require('./tags.js').default,
SocialMedia: createSocialMedia, SocialMedia: require('./socialMedia.js').default,
Location: createLocation, Location: require('./locations.js').default,
EmailAddress: createEmailAddress, EmailAddress: require('./emailAddresses.js').default,
UnverifiedEmailAddress: createUnverifiedEmailAddresss, UnverifiedEmailAddress: require('./unverifiedEmailAddresses.js').default,
Donations: createDonations, Donations: require('./donations.js').default,
Report: createReport, Report: require('./reports.js').default,
} }
export const cleanDatabase = async (options = {}) => { export const cleanDatabase = async (options = {}) => {

View File

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

View File

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

View File

@ -1,21 +1,13 @@
import express from 'express' import express from 'express'
import helmet from 'helmet' import helmet from 'helmet'
import { ApolloServer } from 'apollo-server-express' import { ApolloServer } from 'apollo-server-express'
import CONFIG, { requiredConfigs } from './config' import CONFIG from './config'
import middleware from './middleware' import middleware from './middleware'
import { getNeode, getDriver } from './bootstrap/neo4j' import { getNeode, getDriver } from './bootstrap/neo4j'
import decode from './jwt/decode' import decode from './jwt/decode'
import schema from './schema' import schema from './schema'
import webfinger from './activitypub/routes/webfinger' 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 driver = getDriver()
const neode = getNeode() 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", () => { Then("I should see my comment", () => {
cy.get("div.comment p") cy.get("div.comment p")
.should("contain", "Human Connection rocks") .should("contain", "Human Connection rocks")
.get(".ds-avatar img") .get(".user-avatar img")
.should("have.attr", "src") .should("have.attr", "src")
.and("contain", narratorAvatar) .and("contain", narratorAvatar)
.get("div p.ds-text span") .get(".user-teaser > .info > .text")
.should("contain", "today at"); .should("contain", "today at");
}); });

View File

@ -32,5 +32,5 @@ Then("I cannot upload a picture", () => {
cy.get(".ds-card-content") cy.get(".ds-card-content")
.children() .children()
.should("not.have.id", "customdropzone") .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 lastReportTitle
let davidIrvingPostTitle = 'The Truth about the Holocaust' let davidIrvingPostTitle = 'The Truth about the Holocaust'
let davidIrvingPostSlug = 'the-truth-about-the-holocaust' let davidIrvingPostSlug = 'the-truth-about-the-holocaust'
let annoyingUserWhoBlockedModeratorTitle = 'Fake news' let annoyingUserWhoMutedModeratorTitle = 'Fake news'
const savePostTitle = $post => { const savePostTitle = $post => {
return $post return $post
@ -44,7 +44,7 @@ Given('I am logged in with a {string} role', role => {
When('I click on "Report Post" from the content menu of the post', () => { When('I click on "Report Post" from the content menu of the post', () => {
cy.contains('.ds-card', davidIrvingPostTitle) cy.contains('.ds-card', davidIrvingPostTitle)
.find('.content-menu-trigger') .find('.content-menu .base-button')
.click({force: true}) .click({force: true})
cy.get('.popover .ds-menu-item-link') cy.get('.popover .ds-menu-item-link')
@ -54,7 +54,7 @@ When('I click on "Report Post" from the content menu of the post', () => {
When('I click on "Report User" from the content menu in the user info box', () => { When('I click on "Report User" from the content menu in the user info box', () => {
cy.contains('.ds-card', davidIrvingPostTitle) cy.contains('.ds-card', davidIrvingPostTitle)
.get('.user-content-menu .content-menu-trigger') .get('.user-content-menu .base-button')
.click({ force: true }) .click({ force: true })
cy.get('.popover .ds-menu-item-link') cy.get('.popover .ds-menu-item-link')
@ -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', () => { When('I click on the author', () => {
cy.get('.username') cy.get('.user-teaser')
.click() .click()
.url().should('include', '/profile/') .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.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', () => { Then('I can visit the post page', () => {
cy.contains(annoyingUserWhoBlockedModeratorTitle).click() cy.contains(annoyingUserWhoMutedModeratorTitle).click()
cy.location('pathname').should('contain', '/post') cy.location('pathname').should('contain', '/post')
.get('h3').should('contain', annoyingUserWhoBlockedModeratorTitle) .get('h3').should('contain', annoyingUserWhoMutedModeratorTitle)
}) })
When("they have a post someone has reported", () => { When("they have a post someone has reported", () => {

View File

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

View File

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

View File

@ -29,10 +29,20 @@ const narratorParams = {
...termsAndConditionsAgreedVersion, ...termsAndConditionsAgreedVersion,
}; };
const annoyingParams = {
email: "spammy-spammer@example.org",
password: "1234",
...termsAndConditionsAgreedVersion
};
Given("I am logged in", () => { Given("I am logged in", () => {
cy.login(loginCredentials); 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", () => { Given("we have a selection of categories", () => {
cy.createCategories("cat0", "just-for-fun"); cy.createCategories("cat0", "just-for-fun");
}); });
@ -227,7 +237,6 @@ Given("I previously created a post", () => {
lastPost.authorId = narratorParams.id lastPost.authorId = narratorParams.id
lastPost.title = "previously created post"; lastPost.title = "previously created post";
lastPost.content = "with some content"; lastPost.content = "with some content";
lastPost.categoryIds = ["cat0"];
cy.factory() cy.factory()
.create("Post", lastPost); .create("Post", lastPost);
}); });
@ -243,7 +252,7 @@ When("I type in the following text:", text => {
}); });
Then("I select a category", () => { Then("I select a category", () => {
cy.get("span") cy.get(".base-button")
.contains("Just for Fun") .contains("Just for Fun")
.click(); .click();
}); });
@ -407,11 +416,6 @@ Then("there are no notifications in the top menu", () => {
}); });
Given("there is an annoying user called {string}", name => { Given("there is an annoying user called {string}", name => {
const annoyingParams = {
email: "spammy-spammer@example.org",
password: "1234",
...termsAndConditionsAgreedVersion
};
cy.factory().create("User", { cy.factory().create("User", {
...annoyingParams, ...annoyingParams,
id: "annoying-user", 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() cy.neode()
.first("User", { .first("User", {
role: 'moderator' role: 'moderator'
}) })
.then(blocked => { .then(mutedUser => {
cy.neode() cy.neode()
.first("User", { .first("User", {
id: 'annoying-user' id: 'annoying-user'
}) })
.relateTo(blocked, "blocked"); .relateTo(mutedUser, "muted");
}); });
}); });
@ -449,7 +453,7 @@ When("I ", name => {
When( When(
"I click on {string} from the content menu in the user info box", "I click on {string} from the content menu in the user info box",
button => { button => {
cy.get(".user-content-menu .content-menu-trigger").click(); cy.get(".user-content-menu .base-button").click();
cy.get(".popover .ds-menu-item-link") cy.get(".popover .ds-menu-item-link")
.contains(button) .contains(button)
.click({ .click({
@ -459,7 +463,7 @@ When(
); );
When("I navigate to my {string} settings page", settingsPage => { 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") cy.get(".avatar-menu-popover")
.find("a[href]") .find("a[href]")
.contains("Settings") .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() cy.neode()
.first("User", { .first("User", {
name name
}) })
.then(blocked => { .then(mutedUser => {
cy.neode() cy.neode()
.first("User", { .first("User", {
name: narratorParams.name 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: Given we have the following user accounts:
| id | name | | id | name |
| u67 | David Irving | | 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: Given we have the following posts in our database:
| authorId | id | title | content | | authorId | id | title | content |
@ -58,16 +58,16 @@ Feature: Report and Moderate
Then I see all the reported posts including the one from above Then I see all the reported posts including the one from above
And each list item links to the post page 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: Given somebody reported the following posts:
| submitterEmail | resourceId | reasonCategory | reasonDescription | | submitterEmail | resourceId | reasonCategory | reasonDescription |
| p2.submitter@example.org | p2 | other | Offensive content | | p2.submitter@example.org | p2 | other | Offensive content |
And my user account has the role "moderator" 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 And I am logged in
When I click on the avatar menu in the top right corner When I click on the avatar menu in the top right corner
And I click on "Moderation" 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 And I can visit the post page
Scenario: Normal user can't see the moderation page Scenario: Normal user can't see the moderation page

View File

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

View File

@ -1,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 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 To prevent him from seeing and interacting with my contributions and also to avoid seeing his/her posts
Background: Background:
@ -8,36 +8,46 @@ Feature: Block a User
And there is an annoying user called "Spammy Spammer" And there is an annoying user called "Spammy Spammer"
And I am logged in And I am logged in
Scenario: Block a user Scenario: Mute a user
Given I am on the profile page of the annoying 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 When I click on "Mute user" from the content menu in the user info box
And I navigate to my "Blocked users" settings page And I navigate to my "Muted users" settings page
Then I can see the following table: Then I can see the following table:
| Avatar | Name | | Avatar | Name |
| | Spammy Spammer | | | Spammy Spammer |
Scenario: Block a previously followed user Scenario: Mute a previously followed user
Given I follow the user "Spammy Spammer" Given I follow the user "Spammy Spammer"
And "Spammy Spammer" wrote a post "Spam Spam Spam" And "Spammy Spammer" wrote a post "Spam Spam Spam"
When I visit the profile page of the annoying user 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 Then the list of posts of this user is empty
And nobody is following the user profile anymore 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: Given we have the following posts in our database:
| id | title | content | | id | title | content |
| im-not-blocked | Post that should be seen | cause I'm not blocked | | im-not-muted | Post that should be seen | cause I'm not muted |
Given "Spammy Spammer" wrote a post "Spam Spam Spam" Given "Spammy Spammer" wrote a post "Spam Spam Spam"
When I search for "Spam" When I search for "Spam"
Then I should see the following posts in the select dropdown: Then I should see the following posts in the select dropdown:
| title | | title |
| Spam Spam Spam | | Spam Spam Spam |
When I block the user "Spammy Spammer" When I mute the user "Spammy Spammer"
And I refresh the page And I refresh the page
And I search for "Spam" And I search for "Spam"
Then the search has no results 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: Then I should see the following posts in the select dropdown:
| title | | title |
| Post that should be seen | | 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 // This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing) // the project's config changing)
const cucumber = require('cypress-cucumber-preprocessor').default const cucumber = require('cypress-cucumber-preprocessor').default
module.exports = on => { const dotenv = require('dotenv')
module.exports = (on, config) => {
// (on, config) => { // (on, config) => {
// `on` is used to hook into various events Cypress emits // `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config // `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()) on('file:preprocessor', cucumber())
return config
} }

View File

@ -60,7 +60,7 @@ Cypress.Commands.add("login", ({ email, password }) => {
.as("submitButton") .as("submitButton")
.click(); .click();
cy.get(".iziToast-message").should("contain", "You are logged in!"); 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) => { Cypress.Commands.add("logout", (email, password) => {

View File

@ -1,9 +1,13 @@
import Factory from '../../backend/src/seed/factories' import Factory from '../../backend/src/seed/factories'
import { getDriver, getNeode } from '../../backend/src/bootstrap/neo4j' import { getDriver, getNeode } from '../../backend/src/bootstrap/neo4j'
import neode from 'neode'
const neo4jDriver = getDriver() const neo4jConfigs = {
const neodeInstance = getNeode() 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 factoryOptions = { neo4jDriver, neodeInstance }
const factory = Factory(factoryOptions) const factory = Factory(factoryOptions)

View File

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

View File

@ -1,9 +1,13 @@
{ {
"name": "human-connection", "name": "human-connection",
"version": "0.2.0", "version": "0.2.2",
"description": "Fullstack and API tests with cypress and cucumber for Human Connection", "description": "Fullstack and API tests with cypress and cucumber for Human Connection",
"author": "Human Connection gGmbh", "author": "Human Connection gGmbh",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/Human-Connection/Human-Connection.git"
},
"cypress-cucumber-preprocessor": { "cypress-cucumber-preprocessor": {
"nonGlobalStepDefinitions": true "nonGlobalStepDefinitions": true
}, },
@ -14,23 +18,23 @@
"cypress:backend": "cd backend && yarn run dev", "cypress:backend": "cd backend && yarn run dev",
"cypress:webapp": "cd webapp && yarn run dev", "cypress:webapp": "cd webapp && yarn run dev",
"cypress:setup": "run-p cypress:backend cypress:webapp", "cypress:setup": "run-p cypress:backend cypress:webapp",
"cypress:run": "cross-env cypress run --browser chromium", "cypress:run": "cross-env cypress run",
"cypress:open": "cross-env cypress open --browser chromium", "cypress:open": "cross-env cypress open",
"cucumber:setup": "cd backend && yarn run dev", "cucumber:setup": "cd backend && yarn run dev",
"cucumber": "wait-on tcp:4000 && cucumber-js --require-module @babel/register --exit", "cucumber": "wait-on tcp:4000 && cucumber-js --require-module @babel/register --exit",
"version": "auto-changelog -p" "release": "standard-version"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.7.7", "@babel/core": "^7.8.3",
"@babel/preset-env": "^7.7.7", "@babel/preset-env": "^7.8.3",
"@babel/register": "^7.7.7", "@babel/register": "^7.8.3",
"auto-changelog": "^1.16.2", "auto-changelog": "^1.16.2",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"codecov": "^3.6.1", "codecov": "^3.6.1",
"cross-env": "^6.0.3", "cross-env": "^6.0.3",
"cucumber": "^6.0.5", "cucumber": "^6.0.5",
"cypress": "^3.8.1", "cypress": "^3.8.2",
"cypress-cucumber-preprocessor": "^1.19.0", "cypress-cucumber-preprocessor": "^2.0.1",
"cypress-file-upload": "^3.5.3", "cypress-file-upload": "^3.5.3",
"cypress-plugin-retries": "^1.5.2", "cypress-plugin-retries": "^1.5.2",
"date-fns": "^2.9.0", "date-fns": "^2.9.0",
@ -41,7 +45,8 @@
"neo4j-driver": "^4.0.1", "neo4j-driver": "^4.0.1",
"neode": "^0.3.7", "neode": "^0.3.7",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"slug": "^2.1.0" "slug": "^2.1.0",
"standard-version": "^7.1.0"
}, },
"resolutions": { "resolutions": {
"set-value": "^2.0.1" "set-value": "^2.0.1"

View File

@ -2,7 +2,8 @@
ROOT_DIR=$(dirname "$0")/.. ROOT_DIR=$(dirname "$0")/..
# BUILD_COMMIT=${TRAVIS_COMMIT:-$(git rev-parse HEAD)} # 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) apps=(nitro-web nitro-backend neo4j maintenance)
tags=($major $major.$minor $major.$minor.$patch) tags=($major $major.$minor $major.$minor.$patch)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,8 @@
*/ */
$color-primary: rgb(23, 181, 63); $color-primary: rgb(23, 181, 63);
$color-primary-light: rgb(96, 214, 98);
$color-primary-dark: rgb(25, 122, 49);
$color-primary-active: rgb(25, 194, 67); $color-primary-active: rgb(25, 194, 67);
$color-primary-inverse: rgb(241, 253, 244); $color-primary-inverse: rgb(241, 253, 244);
$color-secondary: rgb(0, 142, 230); $color-secondary: rgb(0, 142, 230);
@ -13,6 +15,8 @@
$color-success-active: rgb(26, 203, 71); $color-success-active: rgb(26, 203, 71);
$color-success-inverse: rgb(241, 253, 244); $color-success-inverse: rgb(241, 253, 244);
$color-danger: rgb(219, 57, 36); $color-danger: rgb(219, 57, 36);
$color-danger-light: rgb(242, 97, 65);
$color-danger-dark: rgb(158, 43, 28);
$color-danger-active: rgb(224, 81, 62); $color-danger-active: rgb(224, 81, 62);
$color-danger-inverse: rgb(253, 243, 242); $color-danger-inverse: rgb(253, 243, 242);
$color-warning: rgb(230, 121, 25); $color-warning: rgb(230, 121, 25);
@ -236,6 +240,13 @@ $size-height-xlarge: 60px;
$size-height-footer: 64px; $size-height-footer: 64px;
$size-tappable-square: 44px; $size-tappable-square: 44px;
/**
* @tokens Size Width
* @presenter Spacing
*/
$size-width-paginate: 100px;
/** /**
* @tokens Size Avatar * @tokens Size Avatar
* @presenter Spacing * @presenter Spacing
@ -243,8 +254,22 @@ $size-tappable-square: 44px;
$size-avatar-small: 34px; $size-avatar-small: 34px;
$size-avatar-base: 44px; $size-avatar-base: 44px;
$size-avatar-large: 64px; $size-avatar-large: 114px;
$size-avatar-x-large: 114px;
/**
* @tokens Size Buttons
* @presenter Spacing
*/
$size-button-base: 36px;
$size-button-small: 26px;
/**
* @tokens Size Icons
* @presenter Spacing
*/
$size-icon-base: 16px;
/** /**
* @tokens Shadow * @tokens Shadow
@ -291,6 +316,7 @@ $z-index-page-submenu: 2500;
$z-index-page-header: 2000; $z-index-page-header: 2000;
$z-index-page-sidebar: 1500; $z-index-page-sidebar: 1500;
$z-index-sticky: 100; $z-index-sticky: 100;
$z-index-post-card-link: 5;
/** /**
* @tokens Media Query * @tokens Media Query

View File

@ -175,3 +175,7 @@ hr {
overflow-wrap: break-word; overflow-wrap: break-word;
word-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() wrapper = Wrapper()
}) })
it('renders the HcAvatar component', () => { it('renders the UserAvatar component', () => {
wrapper.find('.avatar-menu-trigger').trigger('click') 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', () => { describe('given a userName', () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,9 +2,14 @@
<dropdown class="content-menu" :placement="placement" offset="5"> <dropdown class="content-menu" :placement="placement" offset="5">
<template slot="default" slot-scope="{ toggleMenu }"> <template slot="default" slot-scope="{ toggleMenu }">
<slot name="button" :toggleMenu="toggleMenu"> <slot name="button" :toggleMenu="toggleMenu">
<ds-button class="content-menu-trigger" size="small" ghost @click.prevent="toggleMenu"> <base-button
<base-icon name="ellipsis-v" /> data-test="content-menu-button"
</ds-button> icon="ellipsis-v"
size="small"
circle
ghost
@click="toggleMenu"
/>
</slot> </slot>
</template> </template>
<div slot="popover" slot-scope="{ toggleMenu }" class="content-menu-popover"> <div slot="popover" slot-scope="{ toggleMenu }" class="content-menu-popover">
@ -150,19 +155,19 @@ export default {
icon: 'edit', icon: 'edit',
}) })
} else { } else {
if (this.resource.isBlocked) { if (this.resource.isMuted) {
routes.push({ routes.push({
label: this.$t(`settings.blocked-users.unblock`), label: this.$t(`settings.muted-users.unmute`),
callback: () => { callback: () => {
this.$emit('unblock', this.resource) this.$emit('unmute', this.resource)
}, },
icon: 'user-plus', icon: 'user-plus',
}) })
} else { } else {
routes.push({ routes.push({
label: this.$t(`settings.blocked-users.block`), label: this.$t(`settings.muted-users.mute`),
callback: () => { callback: () => {
this.$emit('block', this.resource) this.$emit('mute', this.resource)
}, },
icon: 'user-times', icon: 'user-times',
}) })

View File

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

View File

@ -38,7 +38,7 @@
<ds-space /> <ds-space />
<client-only> <client-only>
<hc-user :user="currentUser" :trunc="35" /> <user-teaser :user="currentUser" />
</client-only> </client-only>
<ds-space /> <ds-space />
<ds-input <ds-input
@ -100,17 +100,12 @@
<ds-space /> <ds-space />
<div slot="footer" style="text-align: right"> <div slot="footer" style="text-align: right">
<ds-button <base-button data-test="cancel-button" :disabled="loading" @click="$router.back()" danger>
class="cancel-button"
:disabled="loading"
ghost
@click.prevent="$router.back()"
>
{{ $t('actions.cancel') }} {{ $t('actions.cancel') }}
</ds-button> </base-button>
<ds-button type="submit" icon="check" :loading="loading" :disabled="errors" primary> <base-button type="submit" icon="check" :loading="loading" :disabled="errors" filled>
{{ $t('actions.save') }} {{ $t('actions.save') }}
</ds-button> </base-button>
</div> </div>
<ds-space margin-bottom="large" /> <ds-space margin-bottom="large" />
</ds-card> </ds-card>
@ -127,14 +122,14 @@ import locales from '~/locales'
import PostMutations from '~/graphql/PostMutations.js' import PostMutations from '~/graphql/PostMutations.js'
import HcCategoriesSelect from '~/components/CategoriesSelect/CategoriesSelect' import HcCategoriesSelect from '~/components/CategoriesSelect/CategoriesSelect'
import HcTeaserImage from '~/components/TeaserImage/TeaserImage' import HcTeaserImage from '~/components/TeaserImage/TeaserImage'
import HcUser from '~/components/User/User' import UserTeaser from '~/components/UserTeaser/UserTeaser'
export default { export default {
components: { components: {
HcEditor, HcEditor,
HcCategoriesSelect, HcCategoriesSelect,
HcTeaserImage, HcTeaserImage,
HcUser, UserTeaser,
}, },
props: { props: {
contribution: { type: Object, default: () => {} }, contribution: { type: Object, default: () => {} },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -326,85 +326,4 @@ li > p {
margin: 0 0 $space-x-small; 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> </style>

View File

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

View File

@ -65,13 +65,13 @@ describe('EmbedComponent.vue', () => {
}) })
it('shows the description', () => { 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...', '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', () => { 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', () => { 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...', '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', () => { 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) expect(wrapper.vm.showLinkOnly).toBe(true)
}) })
it('opens the data privacy overlay when a user clicks on the preview image', () => { 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) expect(wrapper.vm.showOverlay).toBe(true)
}) })
@ -135,19 +135,19 @@ describe('EmbedComponent.vue', () => {
wrapper.setData({ showOverlay: true }) wrapper.setData({ showOverlay: true })
}) })
it('when user agress', () => { it('when user agrees', () => {
wrapper.find('.ds-button-primary').trigger('click') wrapper.find('[data-test="play-now-button"]').trigger('click')
expect(wrapper.vm.showEmbed).toBe(true) expect(wrapper.vm.showEmbed).toBe(true)
}) })
it('does not show iframe when user clicks to cancel', () => { 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) expect(wrapper.vm.showEmbed).toBe(false)
}) })
describe("doesn't set permanently", () => { describe("doesn't set permanently", () => {
beforeEach(() => { beforeEach(() => {
wrapper.find('.ds-button-primary').trigger('click') wrapper.find('[data-test="play-now-button"]').trigger('click')
}) })
it("if user doesn't give consent", () => { it("if user doesn't give consent", () => {
@ -162,7 +162,7 @@ describe('EmbedComponent.vue', () => {
describe('sets permanently', () => { describe('sets permanently', () => {
beforeEach(() => { beforeEach(() => {
wrapper.find('input[type=checkbox]').trigger('click') 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', () => { it('changes setting permanetly when user requests', () => {
@ -194,7 +194,7 @@ describe('EmbedComponent.vue', () => {
}) })
it('does not display image to click', () => { 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"> <a v-if="showLinkOnly" :href="dataEmbedUrl" rel="noopener noreferrer nofollow" target="_blank">
{{ dataEmbedUrl }} {{ dataEmbedUrl }}
</a> </a>
<ds-container v-else width="small" class="embed-container"> <ds-container v-else width="small" class="embed-component">
<section class="embed-content"> <section class="content">
<div v-if="showEmbed" v-html="embedHtml" class="embed-html" /> <div v-if="showEmbed" v-html="embedHtml" class="html" />
<template v-else> <template v-else>
<img <img
v-if="embedHtml && embedImage" v-if="embedHtml && embedImage"
:src="embedImage" :src="embedImage"
class="embed-preview-image embed-preview-image--clickable" class="preview --clickable"
@click.prevent="openOverlay()" @click.prevent="openOverlay()"
/> />
<img v-else-if="embedImage" :src="embedImage" class="embed-preview-image" /> <img v-else-if="embedImage" :src="embedImage" class="preview" />
</template> </template>
<h4 v-if="embedTitle">{{ embedTitle }}</h4> <h4 v-if="embedTitle">{{ embedTitle }}</h4>
<p v-if="embedDescription">{{ embedDescription }}</p> <p v-if="embedDescription">{{ embedDescription }}</p>
@ -20,25 +20,27 @@
{{ dataEmbedUrl }} {{ dataEmbedUrl }}
</a> </a>
</section> </section>
<aside v-if="showOverlay" class="embed-overlay"> <aside v-if="showOverlay" class="overlay">
<h3>{{ $t('editor.embed.data_privacy_warning') }}</h3> <h3>{{ $t('editor.embed.data_privacy_warning') }}</h3>
<ds-text>{{ $t('editor.embed.data_privacy_info') }} {{ embedPublisher }}</ds-text> <ds-text>{{ $t('editor.embed.data_privacy_info') }} {{ embedPublisher }}</ds-text>
<div class="embed-buttons"> <div class="buttons">
<ds-button primary @click.prevent="allowEmbed()"> <base-button primary @click="allowEmbed()" data-test="play-now-button">
{{ $t('editor.embed.play_now') }} {{ $t('editor.embed.play_now') }}
</ds-button> </base-button>
<ds-button ghost @click.prevent="closeOverlay()">{{ $t('actions.cancel') }}</ds-button> <base-button @click="closeOverlay()" data-test="cancel-button">
{{ $t('actions.cancel') }}
</base-button>
</div> </div>
<label class="embed-checkbox"> <label class="checkbox">
<input type="checkbox" v-model="checkedAlwaysAllowEmbeds" /> <input type="checkbox" v-model="checkedAlwaysAllowEmbeds" />
<span>{{ $t('editor.embed.always_allow') }}</span> <span>{{ $t('editor.embed.always_allow') }}</span>
</label> </label>
</aside> </aside>
<ds-button <base-button
icon="close" icon="close"
ghost
size="small" size="small"
class="embed-close-button" circle
class="close-button"
@click.prevent="removeEmbed()" @click.prevent="removeEmbed()"
/> />
</ds-container> </ds-container>
@ -151,3 +153,86 @@ export default {
}, },
} }
</script> </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> <template>
<ds-flex :gutter="{ lg: 'large' }" class="emotions-flex"> <div class="emotions-button-group">
<div v-for="emotion in Object.keys(PostsEmotionsCountByEmotion)" :key="emotion"> <emotion-button
<ds-flex-item :width="{ lg: '100%' }"> v-for="emotion in Object.keys(PostsEmotionsCountByEmotion)"
<hc-emotions-button :key="emotion"
@toggleEmotion="toggleEmotion" :emojiPath="iconPath(emotion)"
:PostsEmotionsCountByEmotion="PostsEmotionsCountByEmotion"
:iconPath="iconPath(emotion)"
:emotion="emotion" :emotion="emotion"
:emotionCount="PostsEmotionsCountByEmotion[emotion]"
@toggleEmotion="toggleEmotion"
/> />
</ds-flex-item>
</div> </div>
</ds-flex>
</template> </template>
<script> <script>
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import HcEmotionsButton from '~/components/EmotionsButton/EmotionsButton' import EmotionButton from '~/components/EmotionButton/EmotionButton'
import { PostsEmotionsByCurrentUser } from '~/graphql/PostQuery.js' import { PostsEmotionsByCurrentUser } from '~/graphql/PostQuery.js'
import PostMutations from '~/graphql/PostMutations.js' import PostMutations from '~/graphql/PostMutations.js'
export default { export default {
components: { components: {
HcEmotionsButton, EmotionButton,
}, },
props: { props: {
post: { type: Object, default: () => {} }, post: { type: Object, default: () => {} },
@ -113,3 +112,9 @@ export default {
}, },
} }
</script> </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

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

View File

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

View File

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

View File

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

View File

@ -92,7 +92,7 @@ describe('FilterPosts.vue', () => {
it('starts with all categories button active', () => { it('starts with all categories button active', () => {
const wrapper = openFilterPosts() const wrapper = openFilterPosts()
allCategoriesButton = wrapper.findAll('button').at(1) allCategoriesButton = wrapper.findAll('button').at(1)
expect(allCategoriesButton.attributes().class).toContain('ds-button-primary') expect(allCategoriesButton.attributes().class).toContain('--filled')
}) })
it('calls TOGGLE_CATEGORY when clicked', () => { it('calls TOGGLE_CATEGORY when clicked', () => {
@ -111,35 +111,35 @@ describe('FilterPosts.vue', () => {
expect(mutations['posts/TOGGLE_LANGUAGE']).toHaveBeenCalledWith({}, 'en') expect(mutations['posts/TOGGLE_LANGUAGE']).toHaveBeenCalledWith({}, 'en')
}) })
it('sets category button attribute `primary` when corresponding category is filtered', () => { it('sets category button attribute `filled` when corresponding category is filtered', () => {
getters['posts/filteredCategoryIds'] = jest.fn(() => ['cat9']) getters['posts/filteredCategoryIds'] = jest.fn(() => ['cat9'])
const wrapper = openFilterPosts() const wrapper = openFilterPosts()
democracyAndPoliticsButton = wrapper.findAll('button').at(4) democracyAndPoliticsButton = wrapper.findAll('button').at(4)
expect(democracyAndPoliticsButton.attributes().class).toContain('ds-button-primary') expect(democracyAndPoliticsButton.attributes().class).toContain('--filled')
}) })
it('sets language button attribute `primary` when corresponding language is filtered', () => { it('sets language button attribute `filled` when corresponding language is filtered', () => {
getters['posts/filteredLanguageCodes'] = jest.fn(() => ['es']) getters['posts/filteredLanguageCodes'] = jest.fn(() => ['es'])
const wrapper = openFilterPosts() const wrapper = openFilterPosts()
spanishButton = wrapper spanishButton = wrapper
.findAll('button.language-buttons') .findAll('button.language-buttons')
.at(languages.findIndex(l => l.code === 'es')) .at(languages.findIndex(l => l.code === 'es'))
expect(spanishButton.attributes().class).toContain('ds-button-primary') expect(spanishButton.attributes().class).toContain('--filled')
}) })
it('sets "filter-by-followed-authors-only" button attribute `primary`', () => { it('sets "filter-by-followed" button attribute `filled`', () => {
getters['posts/filteredByUsersFollowed'] = jest.fn(() => true) getters['posts/filteredByUsersFollowed'] = jest.fn(() => true)
const wrapper = openFilterPosts() const wrapper = openFilterPosts()
expect( expect(wrapper.find('.base-button[data-test="filter-by-followed"]').classes('--filled')).toBe(
wrapper.find({ name: 'filter-by-followed-authors-only' }).classes('ds-button-primary'), true,
).toBe(true) )
}) })
describe('click "filter-by-followed-authors-only" button', () => { describe('click "filter-by-followed" button', () => {
let wrapper let wrapper
beforeEach(() => { beforeEach(() => {
wrapper = openFilterPosts() wrapper = openFilterPosts()
wrapper.find({ name: 'filter-by-followed-authors-only' }).trigger('click') wrapper.find('.base-button[data-test="filter-by-followed"]').trigger('click')
}) })
it('calls TOGGLE_FILTER_BY_FOLLOWED', () => { it('calls TOGGLE_FILTER_BY_FOLLOWED', () => {
@ -150,7 +150,7 @@ describe('FilterPosts.vue', () => {
describe('click on an "emotions-buttons" button', () => { describe('click on an "emotions-buttons" button', () => {
it('calls TOGGLE_EMOTION when clicked', () => { it('calls TOGGLE_EMOTION when clicked', () => {
const wrapper = openFilterPosts() const wrapper = openFilterPosts()
happyEmotionButton = wrapper.findAll('button.emotions-buttons').at(1) happyEmotionButton = wrapper.findAll('.emotion-button .base-button').at(1)
happyEmotionButton.trigger('click') happyEmotionButton.trigger('click')
expect(mutations['posts/TOGGLE_EMOTION']).toHaveBeenCalledWith({}, 'happy') expect(mutations['posts/TOGGLE_EMOTION']).toHaveBeenCalledWith({}, 'happy')
}) })
@ -158,7 +158,7 @@ describe('FilterPosts.vue', () => {
it('sets the attribute `src` to colorized image', () => { it('sets the attribute `src` to colorized image', () => {
getters['posts/filteredByEmotions'] = jest.fn(() => ['happy']) getters['posts/filteredByEmotions'] = jest.fn(() => ['happy'])
const wrapper = openFilterPosts() 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') const happyEmotionButtonImage = happyEmotionButton.find('img')
expect(happyEmotionButtonImage.attributes().src).toEqual('/img/svg/emoji/happy_color.svg') expect(happyEmotionButtonImage.attributes().src).toEqual('/img/svg/emoji/happy_color.svg')
}) })

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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