From 3f28dca8f5e3d73b1b32b8ccd465b902f516674e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Fri, 5 Oct 2018 16:58:20 +0200 Subject: [PATCH 01/21] Install and setup feathers-vuex --- helpers/createApiClient.js | 72 ++++++++++++++++++++++++++++++++++ nuxt.config.js | 1 + package.json | 1 + plugins/api.js | 69 +++++--------------------------- plugins/init-feathers-vuex.js | 19 +++++++++ store/init-feathers-vuex.js | 13 ++++++ store/services/users.js | 11 ++++++ store/services/usersettings.js | 11 ++++++ yarn.lock | 60 +++++++++++++++++++++++++++- 9 files changed, 195 insertions(+), 62 deletions(-) create mode 100644 helpers/createApiClient.js create mode 100644 plugins/init-feathers-vuex.js create mode 100644 store/init-feathers-vuex.js create mode 100644 store/services/users.js create mode 100644 store/services/usersettings.js diff --git a/helpers/createApiClient.js b/helpers/createApiClient.js new file mode 100644 index 00000000..44c8ba0c --- /dev/null +++ b/helpers/createApiClient.js @@ -0,0 +1,72 @@ +import feathers from '@feathersjs/feathers' +import socketio from '@feathersjs/socketio-client' +import io from 'socket.io-client' +import authentication from '@feathersjs/authentication-client' +import urlHelper from '~/helpers/urls' +import Cookie from 'cookie-universal' + +const authKey = 'feathers-jwt' +let socket + +const getSocket = (app) => { + if (socket) return socket + + const endpoint = urlHelper.buildEndpointURL(app.$env.API_HOST, { port: app.$env.API_PORT }) + if (process.env.ENV === 'production') { + socket = socketio(io(endpoint), { timeout: 20000 }) + if (process.server) { + setTimeout(() => { + // close server connection as content was delivered already after 30 seconds at latest + try { + socket.close() + } catch (err) { + console.log(err) + } + }, 30000) + } + } else { + socket = socketio(io(endpoint)) + } + + return socket +} + +let createApiClient = ({app, req, res}) => { + const cookies = Cookie(req, res) + const storageMapping = { + getItem: (key) => { + const res = cookies.get(key) + // console.log(`## STORAGE: getItem(${key})`, res) + return res + }, + setItem: (key, value, options) => { + const res = cookies.set(key, value, options) + // console.log(`## STORAGE: setItem(${key}, ${value}, ${options})`, res) + return res + }, + removeItem: (key) => { + const res = cookies.remove(key) + // console.log(`## STORAGE: removeItem(${key})`, res) + return res + }, + clear: () => { + const res = cookies.removeAll() + if (process.env.NODE_ENV === 'development') { + console.log(`## STORAGE: clear()`, res) + } + return res + } + } + + let api = feathers() + .configure(getSocket(app)) + .configure(authentication({ + storage: storageMapping, + storageKey: authKey, + cookie: authKey + })) + + return api +} + +export default createApiClient diff --git a/nuxt.config.js b/nuxt.config.js index 7bac7f5d..46131328 100644 --- a/nuxt.config.js +++ b/nuxt.config.js @@ -108,6 +108,7 @@ module.exports = { {src: '~/plugins/debug.js', ssr: false}, {src: '~/plugins/raven-client.js', ssr: false}, {src: '~/plugins/api.js'}, + {src: '~/plugins/init-feathers-vuex.js'}, {src: '~/plugins/vue-directives.js', ssr: false}, {src: '~/plugins/init-store-subscriptions.js', ssr: false}, {src: '~/plugins/keep-alive.js', ssr: false}, diff --git a/package.json b/package.json index 8961b772..44556194 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "express-healthcheck": "~0.1.0", "express-locale": "~1.0.5", "express-session": "~1.15.6", + "feathers-vuex": "^1.5.0", "font-awesome": "~4.7.0", "helmet": "^3.12.1", "i18n-iso-countries": "^3.7.8", diff --git a/plugins/api.js b/plugins/api.js index 4619ef7f..6c8a1a81 100644 --- a/plugins/api.js +++ b/plugins/api.js @@ -1,60 +1,15 @@ -import feathers from '@feathersjs/feathers' -import socketio from '@feathersjs/socketio-client' -import io from 'socket.io-client' -import authentication from '@feathersjs/authentication-client' -import urlHelper from '~/helpers/urls' +import feathersVuex from 'feathers-vuex' import Vue from 'vue' +import Vuex from 'vuex' +import createApiClient from '../helpers/createApiClient' -export default ({app, store, redirect, router}) => { - const authKey = 'feathers-jwt' - const endpoint = urlHelper.buildEndpointURL(app.$env.API_HOST, { port: app.$env.API_PORT }) - const storage = { - getItem: (key) => { - const res = app.$cookies.get(key) - // console.log(`## STORAGE: getItem(${key})`, res) - return res - }, - setItem: (key, value, options) => { - const res = app.$cookies.set(key, value, options) - // console.log(`## STORAGE: setItem(${key}, ${value}, ${options})`, res) - return res - }, - removeItem: (key) => { - const res = app.$cookies.remove(key) - // console.log(`## STORAGE: removeItem(${key})`, res) - return res - }, - clear: () => { - const res = app.$cookies.removeAll() - if (app.$env.NODE_ENV === 'development') { - console.log(`## STORAGE: clear()`, res) - } - return res - } - } +export default ({app, store, redirect, router, req, res}) => { + const api = createApiClient({app, req, res}) + const { FeathersVuex } = feathersVuex(api, { idField: '_id' }) - const socket = io(endpoint) + Vue.use(FeathersVuex) + Vue.use(Vuex) - if (process.server) { - setTimeout(() => { - // close server connection as content was delivered already after 30 seconds at latest - try { - socket.close() - } catch (err) { - app.error(err) - } - }, 30000) - } - - let api = feathers() - .configure(socketio(socket, { - timeout: 20000 - })) - .configure(authentication({ - storage: storage, - storageKey: authKey, - cookie: authKey - })) api.hooks({ before: { all: [ @@ -73,7 +28,7 @@ export default ({app, store, redirect, router}) => { if (app.$env.NODE_ENV === 'development') { console.log('####################') console.error(ctx.error) - console.info('JWT TOKEN: ', app.$cookies.get(authKey)) + // console.info('JWT TOKEN: ', app.$cookies.get(authKey)) console.info('path', ctx.path) console.info('service', ctx.service) console.info('method', ctx.method) @@ -103,7 +58,6 @@ export default ({app, store, redirect, router}) => { // fix issues where we could not log in // when at development await api.passport.logout() - storage.removeItem(authKey) } let user = null const response = await api.authenticate(options) @@ -120,11 +74,6 @@ export default ({app, store, redirect, router}) => { api.channel('authenticated').join(connection) }) - /** - * @deprecated - */ - api.authKey = authKey - // make the api accessible inside vue components Vue.use({ install (Vue) { diff --git a/plugins/init-feathers-vuex.js b/plugins/init-feathers-vuex.js new file mode 100644 index 00000000..4ef7fd81 --- /dev/null +++ b/plugins/init-feathers-vuex.js @@ -0,0 +1,19 @@ +import createApiClient from '../helpers/createApiClient' + +const requireModule = require.context( + // The relative path holding the service modules + '../store/services', + // Whether to look in subfolders + false, + // Only include .js files (prevents duplicate imports) + /.js$/ +) + +export default async ({app, store, req, res}) => { + const feathersClient = createApiClient({app, req, res}) + + const servicePlugins = requireModule.keys().map(modulePath => requireModule(modulePath).default(feathersClient)) + servicePlugins.forEach((servicePlugin) => { + servicePlugin(store) + }) +} diff --git a/store/init-feathers-vuex.js b/store/init-feathers-vuex.js new file mode 100644 index 00000000..4ce4bb78 --- /dev/null +++ b/store/init-feathers-vuex.js @@ -0,0 +1,13 @@ +import { initAuth } from 'feathers-vuex' + +export const actions = { + nuxtServerInit ({ commit, dispatch }, { req }) { + return initAuth({ + commit, + dispatch, + req, + moduleName: 'auth', + cookieName: 'feathers-jwt' + }) + } +} diff --git a/store/services/users.js b/store/services/users.js new file mode 100644 index 00000000..ae200954 --- /dev/null +++ b/store/services/users.js @@ -0,0 +1,11 @@ +import feathersVuex from 'feathers-vuex' + +let servicePlugin = (feathersClient) => { + const { service } = feathersVuex(feathersClient, { idField: '_id' }) + const servicePath = 'users' + const servicePlugin = service(servicePath, { + namespace: 'feathers-vuex-users' + }) + return servicePlugin +} +export default servicePlugin diff --git a/store/services/usersettings.js b/store/services/usersettings.js new file mode 100644 index 00000000..233538fc --- /dev/null +++ b/store/services/usersettings.js @@ -0,0 +1,11 @@ +import feathersVuex from 'feathers-vuex' + +let servicePlugin = (feathersClient) => { + const { service } = feathersVuex(feathersClient, { idField: '_id' }) + const servicePath = 'usersettings' + const servicePlugin = service(servicePath, { + namespace: 'feathers-vuex-usersettings' + }) + return servicePlugin +} +export default servicePlugin diff --git a/yarn.lock b/yarn.lock index 44cddb51..bbb12947 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3718,6 +3718,13 @@ debug@^3.0.1, debug@^3.1.0: dependencies: ms "^2.1.1" +debug@^3.2.5: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + debug@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.0.1.tgz#f9bb36d439b8d1f0dd52d8fb6b46e4ebb8c1cd5b" @@ -3732,6 +3739,11 @@ decode-uri-component@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" +deep-diff@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-1.0.2.tgz#afd3d1f749115be965e89c63edc7abb1506b9c26" + integrity sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg== + deep-equal@^1.0.0, deep-equal@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" @@ -4803,6 +4815,11 @@ falafel@^2.1.0: isarray "0.0.1" object-keys "^1.0.6" +fast-copy@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-1.2.3.tgz#2539188451558fd43eadbf4409ff52567f46d2ee" + integrity sha512-ws7blvcPYbGyl/C8Pc2v7cdJerlimT6+FqHW9vp3JvwZ4NfG/fV/cm+Hyglw6hLMFb3Zx3JnM3oEsJPNrcL6Xw== + fast-deep-equal@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" @@ -4845,6 +4862,24 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" +feathers-vuex@^1.5.0: + version "1.6.2" + resolved "https://registry.yarnpkg.com/feathers-vuex/-/feathers-vuex-1.6.2.tgz#08361839e6ffbb19ec9b56085f56fdec20653e2e" + integrity sha512-Qb1QdXNk2UihpNEs2JH2fTmQQOSxdJOaNsqFwd/iaOo305nc/MURaAd6iWDxNYWdIH4hi99Od6m3xnIfA0YQ2g== + dependencies: + "@feathersjs/commons" "^3.0.1" + "@feathersjs/errors" "^3.3.4" + debug "^3.2.5" + deep-diff "^1.0.2" + fast-copy "^1.2.2" + inflection "^1.12.0" + jwt-decode "^2.2.0" + lodash.isobject "^3.0.2" + lodash.merge "^4.6.1" + lodash.trim "^4.5.1" + serialize-error "^2.1.0" + sift "^6.0.0" + figures@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" @@ -5948,6 +5983,11 @@ indexof@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" +inflection@^1.12.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/inflection/-/inflection-1.12.0.tgz#a200935656d6f5f6bc4dc7502e1aecb703228416" + integrity sha1-ogCTVlbW9fa8TcdQLhrstwMihBY= + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -7116,7 +7156,7 @@ just-extend@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-3.0.0.tgz#cee004031eaabf6406da03a7b84e4fe9d78ef288" -jwt-decode@^2.1.0: +jwt-decode@^2.1.0, jwt-decode@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79" @@ -7377,6 +7417,11 @@ lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" +lodash.isobject@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d" + integrity sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0= + lodash.isplainobject@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" @@ -7389,9 +7434,10 @@ lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" -lodash.merge@^4.6.0: +lodash.merge@^4.6.0, lodash.merge@^4.6.1: version "4.6.1" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54" + integrity sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ== lodash.mergewith@^4.6.0: version "4.6.1" @@ -7418,6 +7464,11 @@ lodash.templatesettings@^4.0.0: dependencies: lodash._reinterpolate "~3.0.0" +lodash.trim@^4.5.1: + version "4.5.1" + resolved "https://registry.yarnpkg.com/lodash.trim/-/lodash.trim-4.5.1.tgz#36425e7ee90be4aa5e27bcebb85b7d11ea47aa57" + integrity sha1-NkJefukL5KpeJ7zruFt9EepHqlc= + lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" @@ -10785,6 +10836,11 @@ shuffle-seed@^1.1.6: dependencies: seedrandom "^2.4.2" +sift@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/sift/-/sift-6.0.0.tgz#f93a778e5cbf05a5024ebc391e6b32511a6d1f82" + integrity sha1-+Tp3jly/BaUCTrw5HmsyURptH4I= + sigmund@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" From fe4e13712ecb301ba3e7b7bf9ab969e1cc9640d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Mon, 8 Oct 2018 15:17:01 +0200 Subject: [PATCH 02/21] Follow @appinteractive review ``` session on the server side (SSR) is not destroyed which leads to the following scenario: 1. login as admin (test@test.de) 2. logout 3. login as regular user (test3@test3.de) 4. refresh page after you are logged in 5. see that you are logged in again as the last user (admin) instead of your last loggin (user) You need to remove that session also on the server side, maybe there is a cookie still flying around in the browser which is then send on SSR and the backend responds with the logged in user from the token in the cookie. ``` This is not possible now, as the lazy-loading happens with `app.$api` and not the socket. --- helpers/createApiClient.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/helpers/createApiClient.js b/helpers/createApiClient.js index 44c8ba0c..1f29c124 100644 --- a/helpers/createApiClient.js +++ b/helpers/createApiClient.js @@ -6,11 +6,9 @@ import urlHelper from '~/helpers/urls' import Cookie from 'cookie-universal' const authKey = 'feathers-jwt' -let socket - -const getSocket = (app) => { - if (socket) return socket +const createSocket = (app) => { + let socket const endpoint = urlHelper.buildEndpointURL(app.$env.API_HOST, { port: app.$env.API_PORT }) if (process.env.ENV === 'production') { socket = socketio(io(endpoint), { timeout: 20000 }) @@ -32,6 +30,8 @@ const getSocket = (app) => { } let createApiClient = ({app, req, res}) => { + if (app.$api) return app.$api + const cookies = Cookie(req, res) const storageMapping = { getItem: (key) => { @@ -59,7 +59,7 @@ let createApiClient = ({app, req, res}) => { } let api = feathers() - .configure(getSocket(app)) + .configure(createSocket(app)) .configure(authentication({ storage: storageMapping, storageKey: authKey, From 4adb90e03fcbfc470c994eaab9457bfebc98c76d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Wed, 26 Sep 2018 15:51:25 +0200 Subject: [PATCH 03/21] Implement blacklist on the frontend --- .../Elements/BlockButton/BlockButton.vue | 76 ++++++++++ .../Global/Elements/Follow/FollowButtons.vue | 2 +- locales/de.json | 14 +- locales/en.json | 17 ++- pages/auth/settings.vue | 3 + pages/auth/settings/blacklist.vue | 38 +++++ pages/profile/_slug.vue | 18 +++ plugins/api.js | 6 - plugins/init-feathers-vuex.js | 7 + store/auth.js | 2 + store/index.js | 2 + store/services/usersettings.js | 44 +++++- .../Element/BlockButton/BlockButton.test.js | 139 ++++++++++++++++++ .../Global/Element/Icon/hcIcon.test.js | 8 + test/helpers/testAction.js | 29 ++++ yarn.lock | 60 +------- 16 files changed, 396 insertions(+), 69 deletions(-) create mode 100644 components/Global/Elements/BlockButton/BlockButton.vue create mode 100644 pages/auth/settings/blacklist.vue create mode 100644 test/components/Global/Element/BlockButton/BlockButton.test.js create mode 100644 test/components/Global/Element/Icon/hcIcon.test.js create mode 100644 test/helpers/testAction.js diff --git a/components/Global/Elements/BlockButton/BlockButton.vue b/components/Global/Elements/BlockButton/BlockButton.vue new file mode 100644 index 00000000..8de26cd1 --- /dev/null +++ b/components/Global/Elements/BlockButton/BlockButton.vue @@ -0,0 +1,76 @@ + + + + diff --git a/components/Global/Elements/Follow/FollowButtons.vue b/components/Global/Elements/Follow/FollowButtons.vue index a38c4c99..0bbe076f 100644 --- a/components/Global/Elements/Follow/FollowButtons.vue +++ b/components/Global/Elements/Follow/FollowButtons.vue @@ -17,7 +17,7 @@
diff --git a/locales/de.json b/locales/de.json index 340a89fa..612e8e0c 100644 --- a/locales/de.json +++ b/locales/de.json @@ -58,6 +58,14 @@ "messageFollowSuccess": "Du folgst nun den Aktivitäten von {name}", "messageUnFollowSuccess": "Du folgst den Aktivitäten von {name} nicht mehr" }, + "blacklist": { + "buttonLabelBlock": "Blockieren", + "buttonLabelUnblock": "Blockiert", + "confirmUnfollowTitle": "Nicht mehr Folgen", + "confirmUnfollowMessage": "Möchtest Du {name} nicht mehr folgen und dessen Inhalte nie mehr sehen?", + "blockSuccess": "Du wirst keine Inhalte von {name} mehr sehen", + "unblockSuccess": "Du wirst wieder Inhalte von {name} sehen" + }, "admin": { "activate": "Aktivieren", "addCategory": "Kategorie hinzufügen", @@ -164,6 +172,8 @@ "actionReport": "Beitrag melden", "actionDisable": "Beitrag deaktivieren", "actionEnable": "Beitrag aktivieren", + "actionBlockAuthor": "Autor blockieren", + "actionUnblockAuthor": "Autor freigeben", "bestList": "Bestenliste", "canDos": "Can Do", "canDoAdd": "Can Do starten", @@ -621,7 +631,9 @@ "organizationStreet": "Straße", "organizationZipCode": "PLZ", "organizationCity": "Stadt", - "organizationCountry": "Land" + "organizationCountry": "Land", + "blacklist": "Sperrliste", + "blacklistSubtitle": "Nutzerprofile deren Inhalte Du nicht mehr siehst" } }, "ressource": { diff --git a/locales/en.json b/locales/en.json index 9ea4b565..2d566b17 100644 --- a/locales/en.json +++ b/locales/en.json @@ -14,7 +14,8 @@ "copy403": "Sorry, access to this resource on the server is denied.
Either check the URL, login or go home.", "copy404": "Sorry, the page you're looking for cannot be found.
Either check the URL, go home, or feel free to report this issue.", "copy500": "An error ocurred and your request couldn’t be completed.
Either check the URL, go home, or feel free to report this issue.", - "copy503": "We are currently working on it,
so take a coffee brake and come back a bit later." + "copy503": "We are currently working on it,
so take a coffee brake and come back a bit later.", + "general": "Oops, something went terribly wrong" }, "badges": { "user_role_moderator": "Moderator", @@ -58,6 +59,14 @@ "messageFollowSuccess": "You now follow {name}", "messageUnFollowSuccess": "You not longer follow {name}" }, + "blacklist": { + "buttonLabelBlock": "Block", + "buttonLabelUnblock": "Blocked", + "confirmUnfollowTitle": "Unfollow", + "confirmUnfollowMessage": "Do you want to unfollow and block any content by {name}?", + "blockSuccess": "You will no longer see any content by {name}", + "unblockSuccess": "You will see content by {name} again" + }, "admin": { "activate": "Activate", "addCategory": "add category", @@ -164,6 +173,8 @@ "actionReport": "report post", "actionDisable": "disable post", "actionEnable": "enable post", + "actionBlockAuthor": "block author", + "actionUnblockAuthor": "unblock author", "bestList": "Bestlist", "canDos": "Can Do’s", "canDoAdd": "Start Can Do", @@ -621,7 +632,9 @@ "organizationStreet": "Street", "organizationZipCode": "Zip Code", "organizationCity": "City", - "organizationCountry": "Country" + "organizationCountry": "Country", + "blacklist": "Blacklist", + "blacklistSubtitle": "Users whose content you no longer see" } }, "ressource": { diff --git a/pages/auth/settings.vue b/pages/auth/settings.vue index 3a16bfff..36a496ba 100644 --- a/pages/auth/settings.vue +++ b/pages/auth/settings.vue @@ -24,6 +24,9 @@
  • {{ $t('auth.settings.security', 'Security') }}
  • +
  • + {{ $t('auth.settings.blacklist', 'Blacklist') }} +
  • {{ $t('auth.settings.invites', 'Invites') }}
  • diff --git a/pages/auth/settings/blacklist.vue b/pages/auth/settings/blacklist.vue new file mode 100644 index 00000000..c942ea8b --- /dev/null +++ b/pages/auth/settings/blacklist.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/pages/profile/_slug.vue b/pages/profile/_slug.vue index 8ac3e65c..b141a398 100644 --- a/pages/profile/_slug.vue +++ b/pages/profile/_slug.vue @@ -41,6 +41,8 @@ + +
    @@ -140,6 +142,7 @@ import {mapGetters} from 'vuex' import FollowerItem from '~/components/Profile/FollowerItem/FollowerItem.vue' import FollowButtons from '~/components/Global/Elements/Follow/FollowButtons.vue' + import BlockButton from '~/components/Global/Elements/BlockButton/BlockButton' import Map from '~/components/Map/Map.vue' import Timeline from '~/components/layout/Timeline' import Badges from '~/components/Profile/Badges/Badges' @@ -150,6 +153,7 @@ components: { 'hc-follower-item': FollowerItem, 'hc-follow-buttons': FollowButtons, + 'hc-block-button': BlockButton, 'hc-profile-badges': Badges, 'hc-map': Map, 'hc-timeline': Timeline @@ -261,6 +265,20 @@ } }, methods: { + confirmUnfollow(next){ + const message = this.$t('component.blacklist.confirmUnfollowMessage', { + name: this.user.name || this.$t('component.contribution.creatorUnknown') + }) + this.$dialog.confirm({ + title: this.$t('component.blacklist.confirmUnfollowTitle'), + message, + confirmText: this.$t('button.yes'), + cancelText: this.$t('button.cancel'), + type: 'is-danger', + hasIcon: true, + onConfirm: () => { next() } + }) + }, async onCoverUploadCompleted (value) { this.form.coverImg = value const user = await this.$store.dispatch('auth/patch', { diff --git a/plugins/api.js b/plugins/api.js index 6c8a1a81..fb2d9939 100644 --- a/plugins/api.js +++ b/plugins/api.js @@ -1,14 +1,8 @@ -import feathersVuex from 'feathers-vuex' import Vue from 'vue' -import Vuex from 'vuex' import createApiClient from '../helpers/createApiClient' export default ({app, store, redirect, router, req, res}) => { const api = createApiClient({app, req, res}) - const { FeathersVuex } = feathersVuex(api, { idField: '_id' }) - - Vue.use(FeathersVuex) - Vue.use(Vuex) api.hooks({ before: { diff --git a/plugins/init-feathers-vuex.js b/plugins/init-feathers-vuex.js index 4ef7fd81..3d3ce44b 100644 --- a/plugins/init-feathers-vuex.js +++ b/plugins/init-feathers-vuex.js @@ -1,4 +1,7 @@ +import Vue from 'vue' +import Vuex from 'vuex' import createApiClient from '../helpers/createApiClient' +import feathersVuex from 'feathers-vuex' const requireModule = require.context( // The relative path holding the service modules @@ -11,6 +14,10 @@ const requireModule = require.context( export default async ({app, store, req, res}) => { const feathersClient = createApiClient({app, req, res}) + const { FeathersVuex } = feathersVuex(feathersClient, { idField: '_id' }) + + Vue.use(FeathersVuex) + Vue.use(Vuex) const servicePlugins = requireModule.keys().map(modulePath => requireModule(modulePath).default(feathersClient)) servicePlugins.forEach((servicePlugin) => { diff --git a/store/auth.js b/store/auth.js index 606b8ba6..16840d21 100644 --- a/store/auth.js +++ b/store/auth.js @@ -92,6 +92,7 @@ export const actions = { } commit('SET_USER', user) } + dispatch('feathers-vuex-usersettings/loadCurrent', user, { root: true }) return user }, async checkAuth ({state, getters, commit, dispatch}) { @@ -128,6 +129,7 @@ export const actions = { commit('SET_USER', null) commit('SET_TOKEN', null) const user = await this.app.$api.auth({strategy: 'local', email, password}) + dispatch('feathers-vuex-usersettings/loadCurrent', user, { root: true }) commit('SET_USER', user) diff --git a/store/index.js b/store/index.js index ffcab2a1..7320b41d 100644 --- a/store/index.js +++ b/store/index.js @@ -1,3 +1,5 @@ +export const strict = false + export const actions = { async nuxtServerInit ({dispatch}) { dispatch('categories/init') diff --git a/store/services/usersettings.js b/store/services/usersettings.js index 233538fc..ec61774b 100644 --- a/store/services/usersettings.js +++ b/store/services/usersettings.js @@ -4,7 +4,49 @@ let servicePlugin = (feathersClient) => { const { service } = feathersVuex(feathersClient, { idField: '_id' }) const servicePath = 'usersettings' const servicePlugin = service(servicePath, { - namespace: 'feathers-vuex-usersettings' + namespace: 'feathers-vuex-usersettings', + instanceDefaults: { + blacklist: [] + }, + getters: { + isPending: (state) => { + return ( + state.current || + state.isFindPending || + state.isGetPending || + state.isCreatePending || + state.isUpdatePending || + state.isPatchPending || + state.isRemovePending + ) + } + }, + actions: { + async loadCurrent ({commit, dispatch, state}, user) { + let userId = user._id + let { data } = await dispatch('find', { + query: { userId } + }) + if (data.length > 0) { + commit('setCurrent', data[0]) + } else { + commit('setCurrent', {}) + } + }, + async toggleBlacklist ({commit, dispatch, state}, foreignEntity) { + let current = state.copy + let blacklist = current.blacklist + if (blacklist.includes(foreignEntity._id)) { + blacklist = blacklist.filter(id => id !== foreignEntity._id) + } else { + blacklist.push(foreignEntity._id) + } + current.blacklist = blacklist + commit('setCurrent', current) + if (current._id) { return dispatch('patch', [current._id, current, {}]) } + return current.save() + } + } }) return servicePlugin } diff --git a/test/components/Global/Element/BlockButton/BlockButton.test.js b/test/components/Global/Element/BlockButton/BlockButton.test.js new file mode 100644 index 00000000..52fcd286 --- /dev/null +++ b/test/components/Global/Element/BlockButton/BlockButton.test.js @@ -0,0 +1,139 @@ +import { shallowMount, mount, createLocalVue } from '@vue/test-utils' +import Vuex from 'vuex' +import BlockButton from '~/components/Global/Elements/BlockButton/BlockButton' + +const localVue = createLocalVue() + +localVue.use(Vuex) + +const propsData = { + foreignEntity: { + _id: 4711, + name: 'author' + } +} + +const mocks = { $t: () => {} } + +describe('BlockButton.vue', () => { + let actions + let getters + let store + let wrapper + + beforeEach(() => { + getters = { + 'feathers-vuex-usersettings/isPending': () => false, + 'feathers-vuex-usersettings/current': () => { return { blacklist: [] } } + } + actions = { + 'feathers-vuex-usersettings/toggleBlacklist': jest.fn() + } + store = new Vuex.Store({ + state: {}, getters, actions + }) + }) + + test('renders', () => { + wrapper = shallowMount(BlockButton, { store, localVue, propsData, mocks }) + expect(wrapper.is('div')).toBeTruthy() + }) + + describe('request pending', () => { + beforeEach(() => { + getters['feathers-vuex-usersettings/isPending'] = () => true + store = new Vuex.Store({state: {}, getters, actions}) + wrapper = shallowMount(BlockButton, { store, localVue, propsData, mocks }) + }) + + test('shows a loading spinner', () => { + expect(wrapper.find('hc-button-stub').attributes().isloading).toBeTruthy() + }) + + test('is disabled', () => { + expect(wrapper.find('hc-button-stub').attributes().disabled).toBeTruthy() + }) + }) + + describe('no request pending', () => { + test('is enabled', () => { + wrapper = shallowMount(BlockButton, { store, localVue, propsData, mocks }) + expect(wrapper.findAll('hc-button-stub')).toHaveLength(1) + expect(wrapper.find('hc-button-stub').attributes()).toEqual(expect.not.objectContaining({isloading: 'true'})) + }) + + describe('not blacklisted', () => { + test('shows a neutral ban icon', () => { + expect(wrapper.find('hc-icon-stub').classes()).not.toContain('is-danger') + }) + }) + + describe('is blacklisted', () => { + beforeEach(() => { + getters['feathers-vuex-usersettings/current'] = () => { + return { + blacklist: [4711] + } + } + store = new Vuex.Store({state: {}, getters, actions}) + wrapper = shallowMount(BlockButton, { store, localVue, propsData, mocks }) + }) + + test('gives visual feedback with a red colour', () => { + expect(wrapper.find('hc-icon-stub').classes()).toContain('is-danger') + }) + }) + + test('dispatches feathers-vuex-usersettings/toggleBlacklist on click', () => { + wrapper = mount(BlockButton, { + store, + localVue, + mocks: { + $t: () => {}, + $snackbar: { + open () { } + } + }, + propsData + }) + wrapper.find('button').trigger('click') + expect(actions['feathers-vuex-usersettings/toggleBlacklist']).toHaveBeenCalled() + }) + + describe('given a confirmation callback', () => { + let props + const testCaseAction = () => { + props = Object.assign({}, propsData, { + confirmation: jest.fn() + }) + wrapper = mount(BlockButton, { store, + localVue, + mocks: { $t: () => {}, $snackbar: { open () { } } }, + propsData: props + }) + wrapper.find('button').trigger('click') + } + + describe('not blacklisted', () => { + test('click just unblocks without confirmation', () => { + testCaseAction() + expect(props.confirmation).toHaveBeenCalledTimes(1) + }) + }) + + describe('is blacklisted', () => { + beforeEach(() => { + getters['feathers-vuex-usersettings/current'] = () => { return { blacklist: [4711] } } + store = new Vuex.Store({ + state: {}, getters, actions + }) + }) + + test('click confirms before blocking', () => { + testCaseAction() + expect(props.confirmation).not.toHaveBeenCalled() + }) + }) + }) + }) +}) diff --git a/test/components/Global/Element/Icon/hcIcon.test.js b/test/components/Global/Element/Icon/hcIcon.test.js new file mode 100644 index 00000000..265ccc17 --- /dev/null +++ b/test/components/Global/Element/Icon/hcIcon.test.js @@ -0,0 +1,8 @@ +import { shallowMount } from '@vue/test-utils' +import Icon from '../../../../../components/Global/Elements/Icon/Icon' + +test('It should render an ``.', () => { + const wrapper = shallowMount(Icon) + + expect(wrapper.is('i')).toBeTruthy() +}) diff --git a/test/helpers/testAction.js b/test/helpers/testAction.js new file mode 100644 index 00000000..5860eef2 --- /dev/null +++ b/test/helpers/testAction.js @@ -0,0 +1,29 @@ +/** + * SOURCE: https://vuex.vuejs.org/guide/testing.html + */ +const testAction = async (action, payload, state, expectedMutations, testrunner) => { + let count = 0 + // mock commit + const commit = (type, payload) => { + const mutation = expectedMutations[count] + try { + testrunner.deepEqual(type, mutation.type) + if (payload) { + testrunner.deepEqual(payload, mutation.payload) + } + } catch (error) { + testrunner.fail(new Error(error)) + } + count++ + if (count >= expectedMutations.length) { + testrunner.pass() + } + } + // call the action with mocked store and arguments + await action({ commit, state }, payload) + // check if no mutations should have been dispatched + if (expectedMutations.length === 0) { + testrunner.deepEqual(count, 0) + } +} +module.exports = { testAction } diff --git a/yarn.lock b/yarn.lock index bbb12947..44cddb51 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3718,13 +3718,6 @@ debug@^3.0.1, debug@^3.1.0: dependencies: ms "^2.1.1" -debug@^3.2.5: - version "3.2.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== - dependencies: - ms "^2.1.1" - debug@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.0.1.tgz#f9bb36d439b8d1f0dd52d8fb6b46e4ebb8c1cd5b" @@ -3739,11 +3732,6 @@ decode-uri-component@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" -deep-diff@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-1.0.2.tgz#afd3d1f749115be965e89c63edc7abb1506b9c26" - integrity sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg== - deep-equal@^1.0.0, deep-equal@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" @@ -4815,11 +4803,6 @@ falafel@^2.1.0: isarray "0.0.1" object-keys "^1.0.6" -fast-copy@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-1.2.3.tgz#2539188451558fd43eadbf4409ff52567f46d2ee" - integrity sha512-ws7blvcPYbGyl/C8Pc2v7cdJerlimT6+FqHW9vp3JvwZ4NfG/fV/cm+Hyglw6hLMFb3Zx3JnM3oEsJPNrcL6Xw== - fast-deep-equal@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" @@ -4862,24 +4845,6 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" -feathers-vuex@^1.5.0: - version "1.6.2" - resolved "https://registry.yarnpkg.com/feathers-vuex/-/feathers-vuex-1.6.2.tgz#08361839e6ffbb19ec9b56085f56fdec20653e2e" - integrity sha512-Qb1QdXNk2UihpNEs2JH2fTmQQOSxdJOaNsqFwd/iaOo305nc/MURaAd6iWDxNYWdIH4hi99Od6m3xnIfA0YQ2g== - dependencies: - "@feathersjs/commons" "^3.0.1" - "@feathersjs/errors" "^3.3.4" - debug "^3.2.5" - deep-diff "^1.0.2" - fast-copy "^1.2.2" - inflection "^1.12.0" - jwt-decode "^2.2.0" - lodash.isobject "^3.0.2" - lodash.merge "^4.6.1" - lodash.trim "^4.5.1" - serialize-error "^2.1.0" - sift "^6.0.0" - figures@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" @@ -5983,11 +5948,6 @@ indexof@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" -inflection@^1.12.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/inflection/-/inflection-1.12.0.tgz#a200935656d6f5f6bc4dc7502e1aecb703228416" - integrity sha1-ogCTVlbW9fa8TcdQLhrstwMihBY= - inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -7156,7 +7116,7 @@ just-extend@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-3.0.0.tgz#cee004031eaabf6406da03a7b84e4fe9d78ef288" -jwt-decode@^2.1.0, jwt-decode@^2.2.0: +jwt-decode@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79" @@ -7417,11 +7377,6 @@ lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" -lodash.isobject@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d" - integrity sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0= - lodash.isplainobject@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" @@ -7434,10 +7389,9 @@ lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" -lodash.merge@^4.6.0, lodash.merge@^4.6.1: +lodash.merge@^4.6.0: version "4.6.1" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54" - integrity sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ== lodash.mergewith@^4.6.0: version "4.6.1" @@ -7464,11 +7418,6 @@ lodash.templatesettings@^4.0.0: dependencies: lodash._reinterpolate "~3.0.0" -lodash.trim@^4.5.1: - version "4.5.1" - resolved "https://registry.yarnpkg.com/lodash.trim/-/lodash.trim-4.5.1.tgz#36425e7ee90be4aa5e27bcebb85b7d11ea47aa57" - integrity sha1-NkJefukL5KpeJ7zruFt9EepHqlc= - lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" @@ -10836,11 +10785,6 @@ shuffle-seed@^1.1.6: dependencies: seedrandom "^2.4.2" -sift@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/sift/-/sift-6.0.0.tgz#f93a778e5cbf05a5024ebc391e6b32511a6d1f82" - integrity sha1-+Tp3jly/BaUCTrw5HmsyURptH4I= - sigmund@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" From bef46cb4f73d57ec60d881a97c886a094ee03225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Thu, 27 Sep 2018 15:04:09 +0200 Subject: [PATCH 04/21] Put component tests right next to the component --- .../Global/Elements}/BlockButton/BlockButton.test.js | 2 +- test/components/Global/Element/Icon/hcIcon.test.js | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) rename {test/components/Global/Element => components/Global/Elements}/BlockButton/BlockButton.test.js (98%) delete mode 100644 test/components/Global/Element/Icon/hcIcon.test.js diff --git a/test/components/Global/Element/BlockButton/BlockButton.test.js b/components/Global/Elements/BlockButton/BlockButton.test.js similarity index 98% rename from test/components/Global/Element/BlockButton/BlockButton.test.js rename to components/Global/Elements/BlockButton/BlockButton.test.js index 52fcd286..d66c1952 100644 --- a/test/components/Global/Element/BlockButton/BlockButton.test.js +++ b/components/Global/Elements/BlockButton/BlockButton.test.js @@ -1,6 +1,6 @@ import { shallowMount, mount, createLocalVue } from '@vue/test-utils' import Vuex from 'vuex' -import BlockButton from '~/components/Global/Elements/BlockButton/BlockButton' +import BlockButton from './BlockButton' const localVue = createLocalVue() diff --git a/test/components/Global/Element/Icon/hcIcon.test.js b/test/components/Global/Element/Icon/hcIcon.test.js deleted file mode 100644 index 265ccc17..00000000 --- a/test/components/Global/Element/Icon/hcIcon.test.js +++ /dev/null @@ -1,8 +0,0 @@ -import { shallowMount } from '@vue/test-utils' -import Icon from '../../../../../components/Global/Elements/Icon/Icon' - -test('It should render an ``.', () => { - const wrapper = shallowMount(Icon) - - expect(wrapper.is('i')).toBeTruthy() -}) From 43af37efa18aed587e4f3d76179defa42cdd8956 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Thu, 27 Sep 2018 15:14:54 +0200 Subject: [PATCH 05/21] Simply re-add ava and change the file pattern Thus ava and jest can be used in combination --- yarn.lock | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index 44cddb51..bbb12947 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3718,6 +3718,13 @@ debug@^3.0.1, debug@^3.1.0: dependencies: ms "^2.1.1" +debug@^3.2.5: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + debug@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.0.1.tgz#f9bb36d439b8d1f0dd52d8fb6b46e4ebb8c1cd5b" @@ -3732,6 +3739,11 @@ decode-uri-component@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" +deep-diff@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-1.0.2.tgz#afd3d1f749115be965e89c63edc7abb1506b9c26" + integrity sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg== + deep-equal@^1.0.0, deep-equal@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" @@ -4803,6 +4815,11 @@ falafel@^2.1.0: isarray "0.0.1" object-keys "^1.0.6" +fast-copy@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-1.2.3.tgz#2539188451558fd43eadbf4409ff52567f46d2ee" + integrity sha512-ws7blvcPYbGyl/C8Pc2v7cdJerlimT6+FqHW9vp3JvwZ4NfG/fV/cm+Hyglw6hLMFb3Zx3JnM3oEsJPNrcL6Xw== + fast-deep-equal@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" @@ -4845,6 +4862,24 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" +feathers-vuex@^1.5.0: + version "1.6.2" + resolved "https://registry.yarnpkg.com/feathers-vuex/-/feathers-vuex-1.6.2.tgz#08361839e6ffbb19ec9b56085f56fdec20653e2e" + integrity sha512-Qb1QdXNk2UihpNEs2JH2fTmQQOSxdJOaNsqFwd/iaOo305nc/MURaAd6iWDxNYWdIH4hi99Od6m3xnIfA0YQ2g== + dependencies: + "@feathersjs/commons" "^3.0.1" + "@feathersjs/errors" "^3.3.4" + debug "^3.2.5" + deep-diff "^1.0.2" + fast-copy "^1.2.2" + inflection "^1.12.0" + jwt-decode "^2.2.0" + lodash.isobject "^3.0.2" + lodash.merge "^4.6.1" + lodash.trim "^4.5.1" + serialize-error "^2.1.0" + sift "^6.0.0" + figures@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" @@ -5948,6 +5983,11 @@ indexof@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" +inflection@^1.12.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/inflection/-/inflection-1.12.0.tgz#a200935656d6f5f6bc4dc7502e1aecb703228416" + integrity sha1-ogCTVlbW9fa8TcdQLhrstwMihBY= + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -7116,7 +7156,7 @@ just-extend@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-3.0.0.tgz#cee004031eaabf6406da03a7b84e4fe9d78ef288" -jwt-decode@^2.1.0: +jwt-decode@^2.1.0, jwt-decode@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79" @@ -7377,6 +7417,11 @@ lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" +lodash.isobject@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d" + integrity sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0= + lodash.isplainobject@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" @@ -7389,9 +7434,10 @@ lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" -lodash.merge@^4.6.0: +lodash.merge@^4.6.0, lodash.merge@^4.6.1: version "4.6.1" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54" + integrity sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ== lodash.mergewith@^4.6.0: version "4.6.1" @@ -7418,6 +7464,11 @@ lodash.templatesettings@^4.0.0: dependencies: lodash._reinterpolate "~3.0.0" +lodash.trim@^4.5.1: + version "4.5.1" + resolved "https://registry.yarnpkg.com/lodash.trim/-/lodash.trim-4.5.1.tgz#36425e7ee90be4aa5e27bcebb85b7d11ea47aa57" + integrity sha1-NkJefukL5KpeJ7zruFt9EepHqlc= + lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" @@ -10785,6 +10836,11 @@ shuffle-seed@^1.1.6: dependencies: seedrandom "^2.4.2" +sift@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/sift/-/sift-6.0.0.tgz#f93a778e5cbf05a5024ebc391e6b32511a6d1f82" + integrity sha1-+Tp3jly/BaUCTrw5HmsyURptH4I= + sigmund@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" From 06819936f6e15a3a95838f0bb13702762f32465f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Thu, 27 Sep 2018 15:23:10 +0200 Subject: [PATCH 06/21] Create minimal test for FollowButtons --- .../Elements/Follow/FollowButtons.test.js | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 components/Global/Elements/Follow/FollowButtons.test.js diff --git a/components/Global/Elements/Follow/FollowButtons.test.js b/components/Global/Elements/Follow/FollowButtons.test.js new file mode 100644 index 00000000..9aa5c562 --- /dev/null +++ b/components/Global/Elements/Follow/FollowButtons.test.js @@ -0,0 +1,45 @@ +import { shallowMount, mount, createLocalVue } from '@vue/test-utils' +import Vuex from 'vuex' +import FollowButtons from './FollowButtons' + +const localVue = createLocalVue() + +localVue.use(Vuex) + +const propsData = { + entity: { + _id: 4711, + name: 'author' + } +} + +const mocks = { $t: () => {} } + +describe('FollowButtons.vue', () => { + let wrapper + let actions + let getters + let store + let currentUser + + beforeEach(() => { + currentUser = { + _id: 42 + } + getters = { + 'auth/user': () => { return currentUser }, + } + actions = { + 'connections/syncFollow': () => { + } + } + store = new Vuex.Store({ + state: {}, getters, actions + }) + }) + + test('renders', () => { + wrapper = shallowMount(FollowButtons, { store, localVue, propsData, mocks }) + expect(wrapper.is('div')).toBeTruthy() + }) +}) From 5e3be44226caa4a94b5d744a816a32214c11f073 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Thu, 27 Sep 2018 15:34:38 +0200 Subject: [PATCH 07/21] Place BlockButton into slot in FollowButtons --- .../Elements/BlockButton/BlockButton.test.js | 2 +- .../Elements/BlockButton/BlockButton.vue | 26 ++++++++----------- .../Global/Elements/Follow/FollowButtons.vue | 17 ++---------- pages/profile/_slug.vue | 5 ++-- 4 files changed, 17 insertions(+), 33 deletions(-) diff --git a/components/Global/Elements/BlockButton/BlockButton.test.js b/components/Global/Elements/BlockButton/BlockButton.test.js index d66c1952..5afc8e44 100644 --- a/components/Global/Elements/BlockButton/BlockButton.test.js +++ b/components/Global/Elements/BlockButton/BlockButton.test.js @@ -36,7 +36,7 @@ describe('BlockButton.vue', () => { test('renders', () => { wrapper = shallowMount(BlockButton, { store, localVue, propsData, mocks }) - expect(wrapper.is('div')).toBeTruthy() + expect(wrapper.is('hc-button-stub')).toBeTruthy() }) describe('request pending', () => { diff --git a/components/Global/Elements/BlockButton/BlockButton.vue b/components/Global/Elements/BlockButton/BlockButton.vue index 8de26cd1..4c9b7578 100644 --- a/components/Global/Elements/BlockButton/BlockButton.vue +++ b/components/Global/Elements/BlockButton/BlockButton.vue @@ -1,19 +1,15 @@ diff --git a/store/services/usersettings.js b/store/services/usersettings.js index 79c8f903..223dfae4 100644 --- a/store/services/usersettings.js +++ b/store/services/usersettings.js @@ -29,6 +29,7 @@ let servicePlugin = (feathersClient) => { actions: { async loadCurrent ({commit, dispatch, state}, user) { let userId = user._id + if (!userId) return null let { data } = await dispatch('find', { query: { userId } }) From ea189f70b26de8345daf90e8dde1f251aba2d8ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Mon, 8 Oct 2018 20:26:39 +0200 Subject: [PATCH 13/21] Wrap list of blacklisted users in a loading box --- pages/auth/settings/blacklist.vue | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/pages/auth/settings/blacklist.vue b/pages/auth/settings/blacklist.vue index bf747aa9..90eac33a 100644 --- a/pages/auth/settings/blacklist.vue +++ b/pages/auth/settings/blacklist.vue @@ -4,21 +4,25 @@ {{ $t('auth.settings.blacklist') }}

    {{ $t('auth.settings.blacklistSubtitle') }}

    - + + +
    From 7e596b70a65e031ab9607e2d70a2e06ec917bc1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 13 Nov 2018 16:04:22 +0100 Subject: [PATCH 16/21] Make blacklist a computed property as @appinteractive suggested --- .../Global/Elements/BlockButton/BlockButton.vue | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/components/Global/Elements/BlockButton/BlockButton.vue b/components/Global/Elements/BlockButton/BlockButton.vue index 1ff56c64..2b672bc9 100644 --- a/components/Global/Elements/BlockButton/BlockButton.vue +++ b/components/Global/Elements/BlockButton/BlockButton.vue @@ -3,7 +3,7 @@ :disabled="isPending || isFollowing" :isLoading="isPending" @click="toggleBlacklist"> -