Merge branch 'master' of github.com:Human-Connection/Human-Connection into undo_location_reload

This commit is contained in:
Matt Rider 2019-06-06 12:36:08 -03:00
commit 55458abbff
19 changed files with 566 additions and 160 deletions

View File

@ -43,7 +43,7 @@
}, },
"dependencies": { "dependencies": {
"activitystrea.ms": "~2.1.3", "activitystrea.ms": "~2.1.3",
"apollo-cache-inmemory": "~1.6.0", "apollo-cache-inmemory": "~1.6.1",
"apollo-client": "~2.6.1", "apollo-client": "~2.6.1",
"apollo-link-context": "~1.0.14", "apollo-link-context": "~1.0.14",
"apollo-link-http": "~1.5.14", "apollo-link-http": "~1.5.14",
@ -52,7 +52,7 @@
"cheerio": "~1.0.0-rc.3", "cheerio": "~1.0.0-rc.3",
"cors": "~2.8.5", "cors": "~2.8.5",
"cross-env": "~5.2.0", "cross-env": "~5.2.0",
"date-fns": "2.0.0-alpha.27", "date-fns": "2.0.0-alpha.29",
"debug": "~4.1.1", "debug": "~4.1.1",
"dotenv": "~8.0.0", "dotenv": "~8.0.0",
"express": "~4.17.1", "express": "~4.17.1",

View File

@ -0,0 +1,12 @@
import replaceParams from './replaceParams'
const replaceFilterBubbleParams = async (resolve, root, args, context, resolveInfo) => {
args = await replaceParams(args, context)
return resolve(root, args, context, resolveInfo)
}
export default {
Query: {
Post: replaceFilterBubbleParams,
},
}

View File

@ -0,0 +1,76 @@
import { GraphQLClient } from 'graphql-request'
import { host, login } from '../../jest/helpers'
import Factory from '../../seed/factories'
const factory = Factory()
const currentUserParams = {
email: 'you@example.org',
name: 'This is you',
password: '1234',
}
const followedAuthorParams = {
id: 'u2',
email: 'followed@example.org',
name: 'Followed User',
password: '1234',
}
const randomAuthorParams = {
email: 'someone@example.org',
name: 'Someone else',
password: 'else',
}
beforeEach(async () => {
await Promise.all([
factory.create('User', currentUserParams),
factory.create('User', followedAuthorParams),
factory.create('User', randomAuthorParams),
])
const [asYourself, asFollowedUser, asSomeoneElse] = await Promise.all([
Factory().authenticateAs(currentUserParams),
Factory().authenticateAs(followedAuthorParams),
Factory().authenticateAs(randomAuthorParams),
])
await asYourself.follow({ id: 'u2', type: 'User' })
await asFollowedUser.create('Post', { title: 'This is the post of a followed user' })
await asSomeoneElse.create('Post', { title: 'This is some random post' })
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('FilterBubble middleware', () => {
describe('given an authenticated user', () => {
let authenticatedClient
beforeEach(async () => {
const headers = await login(currentUserParams)
authenticatedClient = new GraphQLClient(host, { headers })
})
describe('no filter bubble', () => {
it('returns all posts', async () => {
const query = '{ Post( filterBubble: {}) { title } }'
const expected = {
Post: [
{ title: 'This is some random post' },
{ title: 'This is the post of a followed user' },
],
}
await expect(authenticatedClient.request(query)).resolves.toEqual(expected)
})
})
describe('filtering for posts of followed users only', () => {
it('returns only posts authored by followed users', async () => {
const query = '{ Post( filterBubble: { author: following }) { title } }'
const expected = {
Post: [{ title: 'This is the post of a followed user' }],
}
await expect(authenticatedClient.request(query)).resolves.toEqual(expected)
})
})
})
})

View File

@ -0,0 +1,31 @@
import { UserInputError } from 'apollo-server'
export default async function replaceParams(args, context) {
const { author = 'all' } = args.filterBubble || {}
const { user } = context
if (author === 'following') {
if (!user)
throw new UserInputError(
"You are unauthenticated - I don't know any users you are following.",
)
const session = context.driver.session()
let { records } = await session.run(
'MATCH(followed:User)<-[:FOLLOWS]-(u {id: $userId}) RETURN followed.id',
{ userId: context.user.id },
)
const followedIds = records.map(record => record.get('followed.id'))
// carefully override `id_in`
args.filter = args.filter || {}
args.filter.author = args.filter.author || {}
args.filter.author.id_in = followedIds
session.close()
}
delete args.filterBubble
return args
}

View File

@ -0,0 +1,129 @@
import replaceParams from './replaceParams.js'
describe('replaceParams', () => {
let args
let context
let run
let action = () => {
return replaceParams(args, context)
}
beforeEach(() => {
args = {}
run = jest.fn().mockResolvedValue({
records: [{ get: () => 1 }, { get: () => 2 }, { get: () => 3 }],
})
context = {
driver: {
session: () => {
return {
run,
close: () => {},
}
},
},
}
})
describe('args == ', () => {
describe('{}', () => {
it('does not crash', async () => {
await expect(action()).resolves.toEqual({})
})
})
describe('unauthenticated user', () => {
beforeEach(() => {
context.user = null
})
describe('{ filterBubble: { author: following } }', () => {
it('throws error', async () => {
args = { filterBubble: { author: 'following' } }
await expect(action()).rejects.toThrow('You are unauthenticated')
})
})
describe('{ filterBubble: { author: all } }', () => {
it('removes filterBubble param', async () => {
const expected = {}
await expect(action()).resolves.toEqual(expected)
})
it('does not make database calls', async () => {
await action()
expect(run).not.toHaveBeenCalled()
})
})
})
describe('authenticated user', () => {
beforeEach(() => {
context.user = { id: 'u4711' }
})
describe('{ filterBubble: { author: following } }', () => {
beforeEach(() => {
args = { filterBubble: { author: 'following' } }
})
it('returns args object with resolved ids of followed users', async () => {
const expected = { filter: { author: { id_in: [1, 2, 3] } } }
await expect(action()).resolves.toEqual(expected)
})
it('makes database calls', async () => {
await action()
expect(run).toHaveBeenCalledTimes(1)
})
describe('given any additional filter args', () => {
describe('merges', () => {
it('empty filter object', async () => {
args.filter = {}
const expected = { filter: { author: { id_in: [1, 2, 3] } } }
await expect(action()).resolves.toEqual(expected)
})
it('filter.title', async () => {
args.filter = { title: 'bla' }
const expected = { filter: { title: 'bla', author: { id_in: [1, 2, 3] } } }
await expect(action()).resolves.toEqual(expected)
})
it('filter.author', async () => {
args.filter = { author: { name: 'bla' } }
const expected = { filter: { author: { name: 'bla', id_in: [1, 2, 3] } } }
await expect(action()).resolves.toEqual(expected)
})
})
})
})
describe('{ filterBubble: { } }', () => {
it('removes filterBubble param', async () => {
const expected = {}
await expect(action()).resolves.toEqual(expected)
})
it('does not make database calls', async () => {
await action()
expect(run).not.toHaveBeenCalled()
})
})
describe('{ filterBubble: { author: all } }', () => {
it('removes filterBubble param', async () => {
const expected = {}
await expect(action()).resolves.toEqual(expected)
})
it('does not make database calls', async () => {
await action()
expect(run).not.toHaveBeenCalled()
})
})
})
})
})

View File

@ -13,6 +13,7 @@ import includedFields from './includedFieldsMiddleware'
import orderBy from './orderByMiddleware' import orderBy from './orderByMiddleware'
import validation from './validation' import validation from './validation'
import notifications from './notifications' import notifications from './notifications'
import filterBubble from './filterBubble/filterBubble'
export default schema => { export default schema => {
const middlewares = { const middlewares = {
@ -30,11 +31,13 @@ export default schema => {
user: user, user: user,
includedFields: includedFields, includedFields: includedFields,
orderBy: orderBy, orderBy: orderBy,
filterBubble: filterBubble,
} }
let order = [ let order = [
'permissions', 'permissions',
'activityPub', 'activityPub',
'filterBubble',
'password', 'password',
'dateTime', 'dateTime',
'validation', 'validation',

View File

@ -1,3 +1,40 @@
enum FilterBubbleAuthorEnum {
following
all
}
input FilterBubble {
author: FilterBubbleAuthorEnum
}
type Query {
Post(
id: ID
activityId: String
objectId: String
title: String
slug: String
content: String
contentExcerpt: String
image: String
imageUpload: Upload
visibility: Visibility
deleted: Boolean
disabled: Boolean
createdAt: String
updatedAt: String
commentsCount: Int
shoutedCount: Int
shoutedByCurrentUser: Boolean
_id: String
first: Int
offset: Int
orderBy: [_PostOrdering]
filter: _PostFilter
filterBubble: FilterBubble
): [Post]
}
type Post { type Post {
id: ID! id: ID!
activityId: String activityId: String
@ -40,4 +77,4 @@ type Post {
RETURN COUNT(u) >= 1 RETURN COUNT(u) >= 1
""" """
) )
} }

View File

@ -1296,18 +1296,18 @@ apollo-cache-control@^0.1.0:
dependencies: dependencies:
graphql-extensions "^0.0.x" graphql-extensions "^0.0.x"
apollo-cache-inmemory@~1.6.0: apollo-cache-inmemory@~1.6.1:
version "1.6.0" version "1.6.1"
resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.0.tgz#a106cdc520f0a043be2575372d5dbb7e4790254c" resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.1.tgz#536b6f366461f6264250041f9146363e2faa1d4c"
integrity sha512-Mr86ucMsXnRH9YRvcuuy6kc3dtyRBuVSo8gdxp2sJVuUAtvQ6r/8E+ok2qX84em9ZBAYxoyvPnKeShhvcKiiDw== integrity sha512-c/WJjh9MTWcdussCTjLKufpPjTx3qOFkBPHIDOOpQ+U0B7K1PczPl9N0LaC4ir3wAWL7s4A0t2EKtoR+6UP92g==
dependencies: dependencies:
apollo-cache "^1.3.0" apollo-cache "^1.3.1"
apollo-utilities "^1.3.0" apollo-utilities "^1.3.1"
optimism "^0.9.0" optimism "^0.9.0"
ts-invariant "^0.4.0" ts-invariant "^0.4.0"
tslib "^1.9.3" tslib "^1.9.3"
apollo-cache@1.3.1, apollo-cache@^1.3.0: apollo-cache@1.3.1, apollo-cache@^1.3.1:
version "1.3.1" version "1.3.1"
resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.3.1.tgz#c015f93a9a7f32b3eeea0c471addd6e854da754c" resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.3.1.tgz#c015f93a9a7f32b3eeea0c471addd6e854da754c"
integrity sha512-BJ/Mehr3u6XCaHYSmgZ6DM71Fh30OkW6aEr828WjHvs+7i0RUuP51/PM7K6T0jPXtuw7UbArFFPZZsNgXnyyJA== integrity sha512-BJ/Mehr3u6XCaHYSmgZ6DM71Fh30OkW6aEr828WjHvs+7i0RUuP51/PM7K6T0jPXtuw7UbArFFPZZsNgXnyyJA==
@ -1559,7 +1559,7 @@ apollo-upload-server@^7.0.0:
http-errors "^1.7.0" http-errors "^1.7.0"
object-path "^0.11.4" object-path "^0.11.4"
apollo-utilities@1.3.1, apollo-utilities@^1.0.1, apollo-utilities@^1.2.1, apollo-utilities@^1.3.0, apollo-utilities@^1.3.1: apollo-utilities@1.3.1, apollo-utilities@^1.0.1, apollo-utilities@^1.2.1, apollo-utilities@^1.3.1:
version "1.3.1" version "1.3.1"
resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.1.tgz#4c45f9b52783c324e2beef822700bdea374f82d1" resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.1.tgz#4c45f9b52783c324e2beef822700bdea374f82d1"
integrity sha512-P5cJ75rvhm9hcx9V/xCW0vlHhRd0S2icEcYPoRYNTc5djbynpuO+mQuJ4zMHgjNDpvvDxDfZxXTJ6ZUuJZodiQ== integrity sha512-P5cJ75rvhm9hcx9V/xCW0vlHhRd0S2icEcYPoRYNTc5djbynpuO+mQuJ4zMHgjNDpvvDxDfZxXTJ6ZUuJZodiQ==
@ -2579,10 +2579,10 @@ data-urls@^1.0.0:
whatwg-mimetype "^2.2.0" whatwg-mimetype "^2.2.0"
whatwg-url "^7.0.0" whatwg-url "^7.0.0"
date-fns@2.0.0-alpha.27: date-fns@2.0.0-alpha.29:
version "2.0.0-alpha.27" version "2.0.0-alpha.29"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-alpha.27.tgz#5ecd4204ef0e7064264039570f6e8afbc014481c" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-alpha.29.tgz#9d4a36e3ebba63d009e957fea8fdfef7921bc6cb"
integrity sha512-cqfVLS+346P/Mpj2RpDrBv0P4p2zZhWWvfY5fuWrXNR/K38HaAGEkeOwb47hIpQP9Jr/TIxjZ2/sNMQwdXuGMg== integrity sha512-AIFZ0hG/1fdb7HZHTDyiEJdNiaFyZxXcx/kF8z3I9wxbhkN678KrrLSneKcsb0Xy5KqCA4wCIxmGpdVWSNZnpA==
debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
version "2.6.9" version "2.6.9"

View File

@ -12,14 +12,16 @@ Then('I should be able to change my profile picture', () => {
cy.fixture(avatarUpload, 'base64').then(fileContent => { cy.fixture(avatarUpload, 'base64').then(fileContent => {
cy.get('#customdropzone').upload( cy.get('#customdropzone').upload(
{ fileContent, fileName: avatarUpload, mimeType: 'image/png' }, { fileContent, fileName: avatarUpload, mimeType: 'image/png' },
{ subjectType: 'drag-n-drop' }, { subjectType: 'drag-n-drop' }
) )
}) })
cy.get('#customdropzone') cy.get('.profile-avatar img')
.should('have.attr', 'style') .should('have.attr', 'src')
.and('contains', 'onourjourney') .and('contains', 'onourjourney')
cy.contains('.iziToast-message', 'Upload successful') cy.contains('.iziToast-message', 'Upload successful').should(
.should('have.length', 1) 'have.length',
1
)
}) })
When("I visit another user's profile page", () => { When("I visit another user's profile page", () => {
@ -31,4 +33,4 @@ Then('I cannot upload a picture', () => {
.children() .children()
.should('not.have.id', 'customdropzone') .should('not.have.id', 'customdropzone')
.should('have.class', 'ds-avatar') .should('have.class', 'ds-avatar')
}) })

View File

@ -230,7 +230,7 @@ When('I type in the following text:', text => {
Then('the post shows up on the landing page at position {int}', index => { Then('the post shows up on the landing page at position {int}', index => {
cy.openPage('landing') cy.openPage('landing')
const selector = `:nth-child(${index}) > .ds-card > .ds-card-content` const selector = `.post-card:nth-child(${index}) > .ds-card-content`
cy.get(selector).should('contain', lastPost.title) cy.get(selector).should('contain', lastPost.title)
cy.get(selector).should('contain', lastPost.content) cy.get(selector).should('contain', lastPost.content)
}) })

View File

@ -0,0 +1,54 @@
import { mount, createLocalVue } from '@vue/test-utils'
import FilterMenu from './FilterMenu.vue'
import Styleguide from '@human-connection/styleguide'
const localVue = createLocalVue()
localVue.use(Styleguide)
describe('FilterMenu.vue', () => {
let wrapper
let mocks
const createWrapper = mountMethod => {
return mountMethod(FilterMenu, {
mocks,
localVue,
})
}
beforeEach(() => {
mocks = { $t: () => {} }
})
describe('mount', () => {
beforeEach(() => {
wrapper = createWrapper(mount)
})
it('renders a card', () => {
expect(wrapper.is('.ds-card')).toBe(true)
})
describe('click "filter-by-followed-authors-only" button', () => {
it('emits filterBubble object', () => {
wrapper.find({ name: 'filter-by-followed-authors-only' }).trigger('click')
expect(wrapper.emitted('changeFilterBubble')).toBeTruthy()
})
it('toggles filterBubble.author property', () => {
wrapper.find({ name: 'filter-by-followed-authors-only' }).trigger('click')
expect(wrapper.emitted('changeFilterBubble')[0]).toEqual([{ author: 'following' }])
wrapper.find({ name: 'filter-by-followed-authors-only' }).trigger('click')
expect(wrapper.emitted('changeFilterBubble')[1]).toEqual([{ author: 'all' }])
})
it('makes button primary', () => {
wrapper.find({ name: 'filter-by-followed-authors-only' }).trigger('click')
expect(
wrapper.find({ name: 'filter-by-followed-authors-only' }).classes('ds-button-primary'),
).toBe(true)
})
})
})
})

View File

@ -0,0 +1,57 @@
<template>
<ds-card>
<ds-flex>
<ds-flex-item class="filter-menu-title">
<ds-heading size="h3">
{{ $t('filter-menu.title') }}
</ds-heading>
</ds-flex-item>
<ds-flex-item>
<div class="filter-menu-buttons">
<ds-button
name="filter-by-followed-authors-only"
icon="user-plus"
:primary="onlyFollowed"
@click="toggleOnlyFollowed"
/>
</div>
</ds-flex-item>
</ds-flex>
</ds-card>
</template>
<script>
export default {
data() {
// We have to fix styleguide here. It uses .includes wich will always be
// false for arrays of objects.
return {
filterBubble: {
author: 'all',
},
}
},
computed: {
onlyFollowed() {
return this.filterBubble.author === 'following'
},
},
methods: {
toggleOnlyFollowed() {
this.filterBubble.author = this.onlyFollowed ? 'all' : 'following'
this.$emit('changeFilterBubble', this.filterBubble)
},
},
}
</script>
<style lang="scss">
.filter-menu-title {
display: flex;
align-items: center;
}
.filter-menu-buttons {
float: right;
}
</style>

View File

@ -4,21 +4,30 @@
id="customdropzone" id="customdropzone"
:key="user.avatar" :key="user.avatar"
ref="el" ref="el"
:use-custom-slot="true"
:options="dropzoneOptions" :options="dropzoneOptions"
:include-styling="false"
:style="backgroundImage"
@vdropzone-thumbnail="thumbnail"
@vdropzone-error="verror" @vdropzone-error="verror"
/> >
<div class="dz-message" @mouseover="hover = true" @mouseleave="hover = false">
<hc-avatar :user="user" class="profile-avatar" size="x-large"></hc-avatar>
<div class="hc-attachments-upload-area">
<div class="hc-drag-marker">
<ds-icon v-if="hover" name="image" size="xxx-large" />
</div>
</div>
</div>
</vue-dropzone>
</div> </div>
</template> </template>
<script> <script>
import vueDropzone from 'nuxt-dropzone' import vueDropzone from 'nuxt-dropzone'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import HcAvatar from '~/components/Avatar/Avatar.vue'
export default { export default {
components: { components: {
vueDropzone, vueDropzone,
HcAvatar,
}, },
props: { props: {
user: { type: Object, default: null }, user: { type: Object, default: null },
@ -29,22 +38,11 @@ export default {
url: this.vddrop, url: this.vddrop,
maxFilesize: 5.0, maxFilesize: 5.0,
previewTemplate: this.template(), previewTemplate: this.template(),
dictDefaultMessage: '',
}, },
error: false, error: false,
hover: false,
} }
}, },
computed: {
backgroundImage() {
const avatar =
this.user.avatar ||
'https://human-connection.org/wp-content/uploads/2019/03/human-connection-logo.svg'
const userAvatar = avatar.startsWith('/') ? avatar.replace('/', '/api/') : avatar
return {
backgroundImage: `url(${userAvatar})`,
}
},
},
watch: { watch: {
error() { error() {
let that = this let that = this
@ -62,20 +60,6 @@ export default {
</div> </div>
` `
}, },
thumbnail(file, dataUrl) {
let j, len, ref, thumbnailElement
if (file.previewElement) {
this.$refs.el.$el.style.backgroundImage = ''
file.previewElement.classList.remove('dz-file-preview')
ref = file.previewElement.querySelectorAll('[data-dz-thumbnail-bg]')
for (j = 0, len = ref.length; j < len; j++) {
thumbnailElement = ref[j]
thumbnailElement.alt = file.name
thumbnailElement.style.backgroundImage = 'url("' + dataUrl + '")'
}
file.previewElement.classList.add('dz-image-preview')
}
},
vddrop(file) { vddrop(file) {
const avatarUpload = file[0] const avatarUpload = file[0]
this.$apollo this.$apollo
@ -107,25 +91,7 @@ export default {
}, },
} }
</script> </script>
<style> <style lang="scss">
#customdropzone {
margin: -60px auto auto;
width: 122px;
min-height: 122px;
background-size: cover;
background-repeat: no-repeat;
border-radius: 50%;
font-family: 'Arial', sans-serif;
letter-spacing: 0.2px;
color: #777;
transition: background-color 0.2s linear;
padding: 40px;
}
#customdropzone:hover {
cursor: pointer;
}
#customdropzone .dz-preview { #customdropzone .dz-preview {
transition: all 0.2s ease-out; transition: all 0.2s ease-out;
width: 160px; width: 160px;
@ -150,15 +116,59 @@ export default {
width: 100%; width: 100%;
} }
#customdropzone .dz-preview .dz-details { .hc-attachments-upload-area {
color: white; position: relative;
transition: opacity 0.2s linear; display: flex;
text-align: center; align-items: center;
justify-content: center;
cursor: pointer;
} }
#customdropzone .dz-success-mark, .hc-attachments-upload-button {
.dz-error-mark, pointer-events: none;
.dz-remove { }
display: none;
.hc-drag-marker {
position: relative;
width: 122px;
height: 122px;
border-radius: 100%;
display: flex;
align-items: center;
justify-content: center;
color: hsl(0, 0%, 25%);
transition: all 0.2s ease-out;
font-size: 60px;
margin: -120px auto 5px;
background-color: rgba(255, 255, 255, 0.3);
opacity: 0.1;
&:before {
position: absolute;
content: '';
top: 0;
left: 0;
bottom: 0;
right: 0;
border-radius: 100%;
border: 20px solid rgba(255, 255, 255, 0.4);
visibility: hidden;
}
&:after {
position: absolute;
content: '';
top: 10px;
left: 10px;
bottom: 10px;
right: 10px;
border-radius: 100%;
border: 1px dashed hsl(0, 0%, 25%);
}
.hc-attachments-upload-area:hover & {
opacity: 1;
}
} }
</style> </style>

View File

@ -35,26 +35,6 @@ describe('Upload', () => {
}, },
} }
const fileSuccess = {
filename: 'avatar.jpg',
previewElement: {
classList: {
remove: jest.fn(),
add: jest.fn(),
},
querySelectorAll: jest.fn().mockReturnValue([
{
alt: '',
style: {
'background-image': '/api/generic.jpg',
},
},
]),
},
}
const dataUrl = 'avatar.jpg'
beforeEach(() => { beforeEach(() => {
jest.useFakeTimers() jest.useFakeTimers()
wrapper = shallowMount(Upload, { localVue, propsData, mocks }) wrapper = shallowMount(Upload, { localVue, propsData, mocks })
@ -69,11 +49,6 @@ describe('Upload', () => {
expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1) expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1)
}) })
it('thumbnail', () => {
wrapper.vm.thumbnail(fileSuccess, dataUrl)
expect(fileSuccess.previewElement.classList.add).toHaveBeenCalledTimes(1)
})
describe('error handling', () => { describe('error handling', () => {
const message = 'File upload failed' const message = 'File upload failed'
const fileError = { status: 'error' } const fileError = { status: 'error' }
@ -93,5 +68,15 @@ describe('Upload', () => {
jest.runAllTimers() jest.runAllTimers()
expect(wrapper.vm.error).toEqual(false) expect(wrapper.vm.error).toEqual(false)
}) })
it('shows an error toaster when the apollo mutation rejects', async () => {
// calls vddrop twice because of how mockResolvedValueOnce works in jest
// the first time the mock function is called it will resolve, calling it a
// second time will cause it to fail(with this implementation)
// https://jestjs.io/docs/en/mock-function-api.html#mockfnmockresolvedvalueoncevalue
await wrapper.vm.vddrop([{ filename: 'avatar.jpg' }])
await wrapper.vm.vddrop([{ filename: 'avatar.jpg' }])
expect(mocks.$toast.error).toHaveBeenCalledTimes(1)
})
}) })
}) })

View File

@ -1,4 +1,7 @@
{ {
"filter-menu": {
"title": "Deine Filterblase"
},
"login": { "login": {
"copy": "Wenn Du bereits ein Konto bei Human Connection hast, melde Dich bitte hier an.", "copy": "Wenn Du bereits ein Konto bei Human Connection hast, melde Dich bitte hier an.",
"login": "Einloggen", "login": "Einloggen",

View File

@ -1,4 +1,7 @@
{ {
"filter-menu": {
"title": "Your filter bubble"
},
"login": { "login": {
"copy": "If you already have a human-connection account, login here.", "copy": "If you already have a human-connection account, login here.",
"login": "Login", "login": "Login",

View File

@ -51,7 +51,7 @@
"dependencies": { "dependencies": {
"@human-connection/styleguide": "0.5.17", "@human-connection/styleguide": "0.5.17",
"@nuxtjs/apollo": "4.0.0-rc4.2", "@nuxtjs/apollo": "4.0.0-rc4.2",
"@nuxtjs/axios": "~5.5.3", "@nuxtjs/axios": "~5.5.4",
"@nuxtjs/dotenv": "~1.3.0", "@nuxtjs/dotenv": "~1.3.0",
"@nuxtjs/style-resources": "~0.1.2", "@nuxtjs/style-resources": "~0.1.2",
"accounting": "~0.4.1", "accounting": "~0.4.1",
@ -59,7 +59,7 @@
"apollo-client": "~2.6.1", "apollo-client": "~2.6.1",
"cookie-universal-nuxt": "~2.0.14", "cookie-universal-nuxt": "~2.0.14",
"cross-env": "~5.2.0", "cross-env": "~5.2.0",
"date-fns": "2.0.0-alpha.27", "date-fns": "2.0.0-alpha.29",
"express": "~4.17.1", "express": "~4.17.1",
"graphql": "~14.3.1", "graphql": "~14.3.1",
"jsonwebtoken": "~8.5.1", "jsonwebtoken": "~8.5.1",
@ -70,12 +70,11 @@
"stack-utils": "^1.0.2", "stack-utils": "^1.0.2",
"string-hash": "^1.1.3", "string-hash": "^1.1.3",
"tiptap": "1.20.1", "tiptap": "1.20.1",
"tiptap-extensions": "1.20.2", "tiptap-extensions": "1.21.0",
"v-tooltip": "~2.0.2", "v-tooltip": "~2.0.2",
"vue-count-to": "~1.0.13", "vue-count-to": "~1.0.13",
"vue-izitoast": "1.1.2", "vue-izitoast": "1.1.2",
"vue-sweetalert-icons": "~3.2.0", "vue-sweetalert-icons": "~3.2.0",
"vue2-dropzone": "^3.5.9",
"vuex-i18n": "~1.11.0", "vuex-i18n": "~1.11.0",
"zxcvbn": "^4.4.2" "zxcvbn": "^4.4.2"
}, },
@ -107,7 +106,7 @@
"nodemon": "~1.19.1", "nodemon": "~1.19.1",
"prettier": "~1.17.1", "prettier": "~1.17.1",
"sass-loader": "~7.1.0", "sass-loader": "~7.1.0",
"tippy.js": "^4.3.2", "tippy.js": "^4.3.3",
"vue-jest": "~3.0.4", "vue-jest": "~3.0.4",
"vue-svg-loader": "~0.12.0" "vue-svg-loader": "~0.12.0"
} }

View File

@ -1,6 +1,9 @@
<template> <template>
<div> <div>
<ds-flex v-if="Post && Post.length" :width="{ base: '100%' }" gutter="base"> <ds-flex :width="{ base: '100%' }" gutter="base">
<ds-flex-item>
<filter-menu @changeFilterBubble="changeFilterBubble" />
</ds-flex-item>
<hc-post-card <hc-post-card
v-for="(post, index) in uniq(Post)" v-for="(post, index) in uniq(Post)"
:key="post.id" :key="post.id"
@ -24,6 +27,7 @@
</template> </template>
<script> <script>
import FilterMenu from '~/components/FilterMenu/FilterMenu.vue'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import uniqBy from 'lodash/uniqBy' import uniqBy from 'lodash/uniqBy'
import HcPostCard from '~/components/PostCard' import HcPostCard from '~/components/PostCard'
@ -31,6 +35,7 @@ import HcLoadMore from '~/components/LoadMore.vue'
export default { export default {
components: { components: {
FilterMenu,
HcPostCard, HcPostCard,
HcLoadMore, HcLoadMore,
}, },
@ -40,6 +45,7 @@ export default {
Post: [], Post: [],
page: 1, page: 1,
pageSize: 10, pageSize: 10,
filterBubble: { author: 'all' },
} }
}, },
computed: { computed: {
@ -51,6 +57,10 @@ export default {
}, },
}, },
methods: { methods: {
changeFilterBubble(filterBubble) {
this.filterBubble = filterBubble
this.$apollo.queries.Post.refresh()
},
uniq(items, field = 'id') { uniq(items, field = 'id') {
return uniqBy(items, field) return uniqBy(items, field)
}, },
@ -66,6 +76,7 @@ export default {
this.page++ this.page++
this.$apollo.queries.Post.fetchMore({ this.$apollo.queries.Post.fetchMore({
variables: { variables: {
filterBubble: this.filterBubble,
first: this.pageSize, first: this.pageSize,
offset: this.offset, offset: this.offset,
}, },
@ -91,8 +102,8 @@ export default {
Post: { Post: {
query() { query() {
return gql(` return gql(`
query Post($first: Int, $offset: Int) { query Post($filterBubble: FilterBubble, $first: Int, $offset: Int) {
Post(first: $first, offset: $offset) { Post(filterBubble: $filterBubble, first: $first, offset: $offset) {
id id
title title
contentExcerpt contentExcerpt
@ -135,6 +146,7 @@ export default {
}, },
variables() { variables() {
return { return {
filterBubble: this.filterBubble,
first: this.pageSize, first: this.pageSize,
offset: 0, offset: 0,
} }

View File

@ -1141,10 +1141,10 @@
vue-cli-plugin-apollo "^0.20.0" vue-cli-plugin-apollo "^0.20.0"
webpack-node-externals "^1.7.2" webpack-node-externals "^1.7.2"
"@nuxtjs/axios@~5.5.3": "@nuxtjs/axios@~5.5.4":
version "5.5.3" version "5.5.4"
resolved "https://registry.yarnpkg.com/@nuxtjs/axios/-/axios-5.5.3.tgz#1893609fc4a3e845516a45c1aaf428104cb3d524" resolved "https://registry.yarnpkg.com/@nuxtjs/axios/-/axios-5.5.4.tgz#c4aee2322901b19d4072670c03144662a73ea6f4"
integrity sha512-WEgWcmgJqWLHWw8ZTieQvuV+I6vcaVgdnCmtzrsKliPsCTtbhDu1i7iFkM/dYAqppIxpL2v46lH6sQ4FfOOxTA== integrity sha512-/Ljsyh5VIc9paXGrQue7RQ+PpBNES1oit0g4l+ya1tfyKnZMpHSbghuLcv0xq+BpXlSEr690uemHbz54/N6U5w==
dependencies: dependencies:
"@nuxtjs/proxy" "^1.3.3" "@nuxtjs/proxy" "^1.3.3"
axios "^0.19.0" axios "^0.19.0"
@ -3747,10 +3747,10 @@ data-urls@^1.0.0:
whatwg-mimetype "^2.2.0" whatwg-mimetype "^2.2.0"
whatwg-url "^7.0.0" whatwg-url "^7.0.0"
date-fns@2.0.0-alpha.27: date-fns@2.0.0-alpha.29:
version "2.0.0-alpha.27" version "2.0.0-alpha.29"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-alpha.27.tgz#5ecd4204ef0e7064264039570f6e8afbc014481c" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-alpha.29.tgz#9d4a36e3ebba63d009e957fea8fdfef7921bc6cb"
integrity sha512-cqfVLS+346P/Mpj2RpDrBv0P4p2zZhWWvfY5fuWrXNR/K38HaAGEkeOwb47hIpQP9Jr/TIxjZ2/sNMQwdXuGMg== integrity sha512-AIFZ0hG/1fdb7HZHTDyiEJdNiaFyZxXcx/kF8z3I9wxbhkN678KrrLSneKcsb0Xy5KqCA4wCIxmGpdVWSNZnpA==
date-now@^0.1.4: date-now@^0.1.4:
version "0.1.4" version "0.1.4"
@ -8997,10 +8997,10 @@ prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.0.1:
prosemirror-state "^1.0.0" prosemirror-state "^1.0.0"
w3c-keyname "^1.1.8" w3c-keyname "^1.1.8"
prosemirror-model@^1.0.0, prosemirror-model@^1.1.0, prosemirror-model@^1.7.0: prosemirror-model@^1.0.0, prosemirror-model@^1.1.0, prosemirror-model@^1.7.0, prosemirror-model@^1.7.1:
version "1.7.0" version "1.7.1"
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.7.0.tgz#1fde0fd7cb2f9ead0be1581ad9f04593002a65aa" resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.7.1.tgz#f140a6e366e1e283aa7a94dbb8c2c7d13139689e"
integrity sha512-/6ul6guiqyAl5I+0qbnL7SlmuX0DEfYqjvzeLUVEnb7nwF/vmKZuWqbjEG2tqi/9SSudvd3UxQTBDHvxy9hQwA== integrity sha512-hYrZPbJvdo2QWERmkCuS80BEf5Rcf3+S28ETr4xu8XKPYjmU6aeQn23G1Fu/2rwqUmk5ZyWYo2nyEsN+Cdv2Qg==
dependencies: dependencies:
orderedmap "^1.0.0" orderedmap "^1.0.0"
@ -10561,43 +10561,43 @@ timsort@^0.3.0:
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
tippy.js@^4.3.2: tippy.js@^4.3.3:
version "4.3.2" version "4.3.3"
resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-4.3.2.tgz#f785d96fd03d890aa118646e1a873f851bd1c8b4" resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-4.3.3.tgz#396304bea577bbff03f2700a1761329e8c1fce86"
integrity sha512-vSdVU8zkhsdCFegwtKq7WJfF29xo4Qiq5GWPZEjKbW4knogI43HJHPAOCUkxbi28gKTTgiWF+GveZgTqhS9QOw== integrity sha512-2fPMlquzVQxpLoOd0eJA1sPZ86/R6zD/9985wV0d2zhhX52DiO3aeg7TTS/mBrUjgFwVZh19YLb4l2c8bJkQPw==
dependencies: dependencies:
popper.js "^1.14.7" popper.js "^1.14.7"
tiptap-commands@^1.10.5, tiptap-commands@^1.10.6: tiptap-commands@^1.10.5, tiptap-commands@^1.10.7:
version "1.10.6" version "1.10.7"
resolved "https://registry.yarnpkg.com/tiptap-commands/-/tiptap-commands-1.10.6.tgz#46f972aacbc8d175248ab7ed7e6183ebc6cc72ed" resolved "https://registry.yarnpkg.com/tiptap-commands/-/tiptap-commands-1.10.7.tgz#c177f875e8a90e05171fdd441190b4b7a769bd27"
integrity sha512-62GrTo3Mmev3AmN0rFDa0gzUFQyN9yTjpuH6xMTo0OqMx6iTluqxdiROB2Hc+9qVCHj6qFwJIG4t8jPrYiuKuw== integrity sha512-Ij62dHkYvOFUcaj+xlbahhoqcY8bFIEho6fXKrxcjjtGzS2DyJnvXmWN6Ow65uZWXkf5Zf8ae6XnYNaKqP3Pyg==
dependencies: dependencies:
prosemirror-commands "^1.0.8" prosemirror-commands "^1.0.8"
prosemirror-inputrules "^1.0.4" prosemirror-inputrules "^1.0.4"
prosemirror-model "^1.7.0" prosemirror-model "^1.7.1"
prosemirror-schema-list "^1.0.3" prosemirror-schema-list "^1.0.3"
prosemirror-state "^1.2.3" prosemirror-state "^1.2.3"
prosemirror-tables "^0.8.1" prosemirror-tables "^0.8.1"
prosemirror-utils "^0.9.0" prosemirror-utils "^0.9.0"
tiptap-utils "^1.5.4" tiptap-utils "^1.5.5"
tiptap-extensions@1.20.2: tiptap-extensions@1.21.0:
version "1.20.2" version "1.21.0"
resolved "https://registry.yarnpkg.com/tiptap-extensions/-/tiptap-extensions-1.20.2.tgz#820c8b1c0c3d2f2176a634c428c22b425f7bbc5f" resolved "https://registry.yarnpkg.com/tiptap-extensions/-/tiptap-extensions-1.21.0.tgz#c2f228144b6943755d2de4617e11febe08c78a42"
integrity sha512-DG5ba2DRKJS11T9B8RMtR/YMkU0PNM25pgDVJ/9MRWBFdD6WFZjyi+fEB9u0uaXRlnyJnYUiV3BScj97UsWl0g== integrity sha512-dIm9Q/G1qL1+sEqQiPXElUzSBcRhXXPuyOdtHgMrncUaCbnaDxsHdkJl700OXfA/GCu7AlhUpd3R67Rmb+voCQ==
dependencies: dependencies:
lowlight "^1.12.1" lowlight "^1.12.1"
prosemirror-collab "^1.1.2" prosemirror-collab "^1.1.2"
prosemirror-history "^1.0.4" prosemirror-history "^1.0.4"
prosemirror-model "^1.7.0" prosemirror-model "^1.7.1"
prosemirror-state "^1.2.3" prosemirror-state "^1.2.3"
prosemirror-tables "^0.8.1" prosemirror-tables "^0.8.1"
prosemirror-transform "^1.1.3" prosemirror-transform "^1.1.3"
prosemirror-utils "^0.9.0" prosemirror-utils "^0.9.0"
prosemirror-view "^1.9.8" prosemirror-view "^1.9.8"
tiptap "^1.21.0" tiptap "^1.21.1"
tiptap-commands "^1.10.6" tiptap-commands "^1.10.7"
tiptap-utils@^1.5.3: tiptap-utils@^1.5.3:
version "1.5.3" version "1.5.3"
@ -10609,12 +10609,12 @@ tiptap-utils@^1.5.3:
prosemirror-tables "^0.8.0" prosemirror-tables "^0.8.0"
prosemirror-utils "^0.8.2" prosemirror-utils "^0.8.2"
tiptap-utils@^1.5.4: tiptap-utils@^1.5.5:
version "1.5.4" version "1.5.5"
resolved "https://registry.yarnpkg.com/tiptap-utils/-/tiptap-utils-1.5.4.tgz#c64b65d305ee70793376c9cec1da242ebf6e1884" resolved "https://registry.yarnpkg.com/tiptap-utils/-/tiptap-utils-1.5.5.tgz#f06c70f0319d9215433b3466b6ece369a078de0f"
integrity sha512-Tl74RM7HZ3v9Eut+cE3DKb+uWM6k0sGvurs9eyCgokrgYmxMMa3CzH1e2c2cmyMB4ErLiY/5v5eMgBKmIZK5Vg== integrity sha512-lnaCmIIkyK050qfsPeMDjoTW0sTqBHxfw1h2GXaX5Nr6JF4VQe0izgf2MI6LCd3PKecJBoW4ce5lNkDpV99+WA==
dependencies: dependencies:
prosemirror-model "^1.7.0" prosemirror-model "^1.7.1"
prosemirror-state "^1.2.3" prosemirror-state "^1.2.3"
prosemirror-tables "^0.8.1" prosemirror-tables "^0.8.1"
prosemirror-utils "^0.9.0" prosemirror-utils "^0.9.0"
@ -10635,21 +10635,21 @@ tiptap@1.20.1:
tiptap-commands "^1.10.5" tiptap-commands "^1.10.5"
tiptap-utils "^1.5.3" tiptap-utils "^1.5.3"
tiptap@^1.21.0: tiptap@^1.21.1:
version "1.21.0" version "1.21.1"
resolved "https://registry.yarnpkg.com/tiptap/-/tiptap-1.21.0.tgz#4d8c1365c611e41c8d4f3d7aa195ddaf891e605b" resolved "https://registry.yarnpkg.com/tiptap/-/tiptap-1.21.1.tgz#c0340375795088b899541b64ce86ae45e98d0369"
integrity sha512-MoOj/8OPMlmoAotIZjAIlUZ59yMMR83xReOw2rGjqbFOooncoY1rLEBp0xz5oe5FLYqoe8dKb+kzOoFERqckVQ== integrity sha512-vCKT/UGorAx1SSX5+9vmtZa+WC+LKfJIArgkEJFXmxfZeyBhNXRSwR+rR+UtdPYi8V4CtmIwtv6eRPP+bH6SWA==
dependencies: dependencies:
prosemirror-commands "^1.0.8" prosemirror-commands "^1.0.8"
prosemirror-dropcursor "^1.1.1" prosemirror-dropcursor "^1.1.1"
prosemirror-gapcursor "^1.0.3" prosemirror-gapcursor "^1.0.3"
prosemirror-inputrules "^1.0.4" prosemirror-inputrules "^1.0.4"
prosemirror-keymap "^1.0.1" prosemirror-keymap "^1.0.1"
prosemirror-model "^1.7.0" prosemirror-model "^1.7.1"
prosemirror-state "^1.2.3" prosemirror-state "^1.2.3"
prosemirror-view "^1.9.8" prosemirror-view "^1.9.8"
tiptap-commands "^1.10.6" tiptap-commands "^1.10.7"
tiptap-utils "^1.5.4" tiptap-utils "^1.5.5"
tmp@^0.0.33: tmp@^0.0.33:
version "0.0.33" version "0.0.33"
@ -11318,13 +11318,6 @@ vue2-dropzone@3.5.8:
dependencies: dependencies:
dropzone "^5.5.1" dropzone "^5.5.1"
vue2-dropzone@^3.5.9:
version "3.5.9"
resolved "https://registry.yarnpkg.com/vue2-dropzone/-/vue2-dropzone-3.5.9.tgz#a63999a45a7aad24d4c21e3d35be409b4e6bdce8"
integrity sha512-nJz6teulVKlZIAeKgvPU7wBI/gzfIgqDOrEp1okSkQIkdprDVCoM0U7XWM0NOM4AAVX+4XGRtMoocYWdTYb3bQ==
dependencies:
dropzone "^5.5.1"
vue@^2.6.10, vue@^2.6.6: vue@^2.6.10, vue@^2.6.6:
version "2.6.10" version "2.6.10"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.10.tgz#a72b1a42a4d82a721ea438d1b6bf55e66195c637" resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.10.tgz#a72b1a42a4d82a721ea438d1b6bf55e66195c637"