Merge branch 'master' of https://github.com/Human-Connection/Human-Connection into 1505_remove_html_in_moderation_view

This commit is contained in:
Wolfgang Huß 2019-09-06 15:52:47 +02:00
commit 40103e84e7
12 changed files with 261 additions and 210 deletions

View File

@ -1,68 +1,37 @@
export const query = (cypher, session) => {
return new Promise((resolve, reject) => {
const data = []
session.run(cypher).subscribe({
onNext: function(record) {
const item = {}
record.keys.forEach(key => {
item[key] = record.get(key)
})
data.push(item)
},
onCompleted: function() {
session.close()
resolve(data)
},
onError: function(error) {
reject(error)
},
})
})
}
const queryOne = (cypher, session) => {
return new Promise((resolve, reject) => {
query(cypher, session)
.then(res => {
resolve(res.length ? res.pop() : {})
})
.catch(err => {
reject(err)
})
})
}
export default {
Query: {
statistics: async (parent, args, { driver, user }) => {
return new Promise(resolve => {
const session = driver.session()
const queries = {
countUsers:
'MATCH (r:User) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countUsers',
countPosts:
'MATCH (r:Post) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countPosts',
countComments:
'MATCH (r:Comment) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countComments',
countNotifications: 'MATCH ()-[r:NOTIFIED]->() RETURN COUNT(r) AS countNotifications',
countInvites: 'MATCH (r:InvitationCode) RETURN COUNT(r) AS countInvites',
countFollows: 'MATCH (:User)-[r:FOLLOWS]->(:User) RETURN COUNT(r) AS countFollows',
countShouts: 'MATCH (:User)-[r:SHOUTED]->(:Post) RETURN COUNT(r) AS countShouts',
const session = driver.session()
const response = {}
try {
const mapping = {
countUsers: 'User',
countPosts: 'Post',
countComments: 'Comment',
countNotifications: 'NOTIFIED',
countInvites: 'InvitationCode',
countFollows: 'FOLLOWS',
countShouts: 'SHOUTED',
}
const data = {
countUsers: queryOne(queries.countUsers, session).then(res => res.countUsers.low),
countPosts: queryOne(queries.countPosts, session).then(res => res.countPosts.low),
countComments: queryOne(queries.countComments, session).then(
res => res.countComments.low,
),
countNotifications: queryOne(queries.countNotifications, session).then(
res => res.countNotifications.low,
),
countInvites: queryOne(queries.countInvites, session).then(res => res.countInvites.low),
countFollows: queryOne(queries.countFollows, session).then(res => res.countFollows.low),
countShouts: queryOne(queries.countShouts, session).then(res => res.countShouts.low),
}
resolve(data)
})
const cypher = `
CALL apoc.meta.stats() YIELD labels, relTypesCount
RETURN labels, relTypesCount
`
const result = await session.run(cypher)
const [statistics] = await result.records.map(record => {
return {
...record.get('labels'),
...record.get('relTypesCount'),
}
})
Object.keys(mapping).forEach(key => {
const stat = statistics[mapping[key]]
response[key] = stat ? stat.toNumber() : 0
})
} finally {
session.close()
}
return response
},
},
}

View File

@ -2,8 +2,6 @@ type Query {
isLoggedIn: Boolean!
# Get the currently logged in User based on the given JWT Token
currentUser: User
# Get the latest Network Statistics
statistics: Statistics!
findPosts(query: String!, limit: Int = 10, filter: _PostFilter): [Post]!
@cypher(
statement: """
@ -39,16 +37,6 @@ type Mutation {
unfollow(id: ID!, type: FollowTypeEnum): Boolean!
}
type Statistics {
countUsers: Int!
countPosts: Int!
countComments: Int!
countNotifications: Int!
countInvites: Int!
countFollows: Int!
countShouts: Int!
}
type Report {
id: ID!
submitter: User @relation(name: "REPORTED", direction: "IN")

View File

@ -0,0 +1,14 @@
type Query {
statistics: Statistics!
}
type Statistics {
countUsers: Int!
countPosts: Int!
countComments: Int!
countNotifications: Int!
countInvites: Int!
countFollows: Int!
countShouts: Int!
}

View File

@ -1,14 +1,14 @@
<template>
<ds-form ref="contributionForm" v-model="form" :schema="formSchema">
<template slot-scope="{ errors }">
<hc-teaser-image :contribution="contribution" @addTeaserImage="addTeaserImage">
<img
v-if="contribution"
class="contribution-image"
:src="contribution.image | proxyApiUrl"
/>
</hc-teaser-image>
<ds-card>
<hc-teaser-image :contribution="contribution" @addTeaserImage="addTeaserImage">
<img
v-if="contribution"
class="contribution-image"
:src="contribution.image | proxyApiUrl"
/>
</hc-teaser-image>
<ds-space />
<hc-user :user="currentUser" :trunc="35" />
<ds-space />

View File

@ -3,6 +3,7 @@
:options="dropzoneOptions"
ref="el"
id="postdropzone"
class="ds-card-image"
:use-custom-slot="true"
@vdropzone-thumbnail="thumbnail"
@vdropzone-error="verror"
@ -10,14 +11,14 @@
<div class="dz-message">
<div
:class="{
'hc-attachments-upload-area-post': createAndUpdate,
'hc-attachments-upload-area-post': true,
'hc-attachments-upload-area-update-post': contribution,
}"
>
<slot></slot>
<div
:class="{
'hc-drag-marker-post': createAndUpdate,
'hc-drag-marker-post': true,
'hc-drag-marker-update-post': contribution,
}"
>
@ -46,7 +47,6 @@ export default {
previewTemplate: this.template(),
},
error: false,
createAndUpdate: true,
}
},
watch: {
@ -75,18 +75,21 @@ export default {
return ''
},
thumbnail: (file, dataUrl) => {
let thumbnailElement, contributionImage, uploadArea
let thumbnailElement, contributionImage, uploadArea, thumbnailPreview, image
if (file.previewElement) {
thumbnailElement = document.querySelectorAll('#postdropzone')[0]
contributionImage = document.querySelectorAll('.contribution-image')[0]
thumbnailPreview = document.querySelectorAll('.thumbnail-preview')[0]
if (contributionImage) {
uploadArea = document.querySelectorAll('.hc-attachments-upload-area-update-post')[0]
uploadArea.removeChild(contributionImage)
uploadArea.classList.remove('hc-attachments-upload-area-update-post')
}
thumbnailElement.classList.add('image-preview')
thumbnailElement.alt = file.name
thumbnailElement.style.backgroundImage = 'url("' + dataUrl + '")'
image = new Image()
image.src = URL.createObjectURL(file)
image.classList.add('thumbnail-preview')
if (thumbnailPreview) return thumbnailElement.replaceChild(image, thumbnailPreview)
thumbnailElement.appendChild(image)
}
},
},
@ -99,25 +102,9 @@ export default {
background-color: $background-color-softest;
}
#postdropzone.image-preview {
background-repeat: no-repeat;
background-size: cover;
background-position: center;
height: auto;
transition: all 0.2s ease-out;
width: 100%;
}
@media only screen and (max-width: 400px) {
#postdropzone.image-preview {
height: 200px;
}
}
@media only screen and (min-width: 401px) and (max-width: 960px) {
#postdropzone.image-preview {
height: 300px;
@media only screen and (max-width: 960px) {
#postdropzone {
min-height: 200px;
}
}

View File

@ -0,0 +1,15 @@
import gql from 'graphql-tag'
export const Statistics = gql`
query {
statistics {
countUsers
countPosts
countComments
countNotifications
countInvites
countFollows
countShouts
}
}
`

View File

@ -22,6 +22,7 @@
},
"site": {
"thanks": "Danke!",
"error-occurred": "Ein Fehler ist aufgetreten.",
"made": "Mit &#10084; gemacht",
"imprint": "Impressum",
"data-privacy": "Datenschutz",

View File

@ -22,6 +22,7 @@
},
"site": {
"thanks": "Thanks!",
"error-occurred": "An error occurred.",
"made": "Made with &#10084;",
"imprint": "Imprint",
"termsAndConditions": "Terms and conditions",

View File

@ -0,0 +1,53 @@
import { mount, createLocalVue } from '@vue/test-utils'
import AdminIndexPage from './index.vue'
import Styleguide from '@human-connection/styleguide'
import VueApollo from 'vue-apollo'
const localVue = createLocalVue()
localVue.use(Styleguide)
localVue.use(VueApollo)
describe('admin/index.vue', () => {
let Wrapper
let store
let mocks
beforeEach(() => {
mocks = {
$t: jest.fn(),
}
})
describe('mount', () => {
Wrapper = () => {
return mount(AdminIndexPage, {
store,
mocks,
localVue,
})
}
describe('in loading state', () => {
beforeEach(() => {
mocks = { ...mocks, $apolloData: { loading: true } }
})
it.skip('shows a loading spinner', () => {
// I don't know how to mock the data that gets passed to
// ApolloQuery component
// What I found:
// https://github.com/Akryum/vue-apollo/issues/656
// https://github.com/Akryum/vue-apollo/issues/609
Wrapper()
const calls = mocks.$t.mock.calls
const expected = [['site.error-occurred']]
expect(calls).toEqual(expected)
})
})
describe('in error state', () => {
it.todo('displays an error message')
})
})
})

View File

@ -1,90 +1,134 @@
<template>
<ds-card>
<client-only>
<ds-space margin="large">
<ds-flex>
<ds-flex-item :width="{ base: '100%', sm: '50%', md: '33%' }">
<ds-space margin="small">
<ds-number :count="0" :label="$t('admin.dashboard.users')" size="x-large" uppercase>
<client-only slot="count">
<hc-count-to :end-val="statistics.countUsers" />
</client-only>
</ds-number>
<ApolloQuery :query="Statistics">
<template v-slot="{ result: { loading, error, data } }">
<template v-if="loading">
<ds-space centered>
<ds-spinner size="large"></ds-spinner>
</ds-space>
</template>
<template v-else-if="error">
<ds-space centered>
<ds-space>
<img :src="errorIconPath" width="40" />
</ds-space>
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', sm: '50%', md: '33%' }">
<ds-space margin="small">
<ds-number :count="0" :label="$t('admin.dashboard.posts')" size="x-large" uppercase>
<client-only slot="count">
<hc-count-to :end-val="statistics.countPosts" />
</client-only>
</ds-number>
</ds-space>
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', sm: '50%', md: '33%' }">
<ds-space margin="small">
<ds-number
:count="0"
:label="$t('admin.dashboard.comments')"
size="x-large"
uppercase
>
<client-only slot="count">
<hc-count-to :end-val="statistics.countComments" />
</client-only>
</ds-number>
</ds-space>
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', sm: '50%', md: '33%' }">
<ds-space margin="small">
<ds-number
:count="0"
:label="$t('admin.dashboard.notifications')"
size="x-large"
uppercase
>
<client-only slot="count">
<hc-count-to :end-val="statistics.countNotifications" />
</client-only>
</ds-number>
</ds-space>
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', sm: '50%', md: '33%' }">
<ds-space margin="small">
<ds-number :count="0" :label="$t('admin.dashboard.invites')" size="x-large" uppercase>
<client-only slot="count">
<hc-count-to :end-val="statistics.countInvites" />
</client-only>
</ds-number>
</ds-space>
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', sm: '50%', md: '33%' }">
<ds-space margin="small">
<ds-number :count="0" :label="$t('admin.dashboard.follows')" size="x-large" uppercase>
<client-only slot="count">
<hc-count-to :end-val="statistics.countFollows" />
</client-only>
</ds-number>
</ds-space>
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', sm: '50%', md: '33%' }">
<ds-space margin="small">
<ds-number :count="0" :label="$t('admin.dashboard.shouts')" size="x-large" uppercase>
<client-only slot="count">
<hc-count-to :end-val="statistics.countShouts" />
</client-only>
</ds-number>
</ds-space>
</ds-flex-item>
</ds-flex>
</ds-space>
</client-only>
<ds-text>
{{ $t('site.error-occurred') }}
</ds-text>
</ds-space>
</template>
<template v-else-if="data">
<ds-space margin="large">
<ds-flex>
<ds-flex-item :width="{ base: '100%', sm: '50%', md: '33%' }">
<ds-space margin="small">
<ds-number
:count="0"
:label="$t('admin.dashboard.users')"
size="x-large"
uppercase
>
<client-only slot="count">
<hc-count-to :end-val="data.statistics.countUsers" />
</client-only>
</ds-number>
</ds-space>
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', sm: '50%', md: '33%' }">
<ds-space margin="small">
<ds-number
:count="0"
:label="$t('admin.dashboard.posts')"
size="x-large"
uppercase
>
<client-only slot="count">
<hc-count-to :end-val="data.statistics.countPosts" />
</client-only>
</ds-number>
</ds-space>
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', sm: '50%', md: '33%' }">
<ds-space margin="small">
<ds-number
:count="0"
:label="$t('admin.dashboard.comments')"
size="x-large"
uppercase
>
<client-only slot="count">
<hc-count-to :end-val="data.statistics.countComments" />
</client-only>
</ds-number>
</ds-space>
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', sm: '50%', md: '33%' }">
<ds-space margin="small">
<ds-number
:count="0"
:label="$t('admin.dashboard.notifications')"
size="x-large"
uppercase
>
<client-only slot="count">
<hc-count-to :end-val="data.statistics.countNotifications" />
</client-only>
</ds-number>
</ds-space>
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', sm: '50%', md: '33%' }">
<ds-space margin="small">
<ds-number
:count="0"
:label="$t('admin.dashboard.invites')"
size="x-large"
uppercase
>
<client-only slot="count">
<hc-count-to :end-val="data.statistics.countInvites" />
</client-only>
</ds-number>
</ds-space>
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', sm: '50%', md: '33%' }">
<ds-space margin="small">
<ds-number
:count="0"
:label="$t('admin.dashboard.follows')"
size="x-large"
uppercase
>
<client-only slot="count">
<hc-count-to :end-val="data.statistics.countFollows" />
</client-only>
</ds-number>
</ds-space>
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', sm: '50%', md: '33%' }">
<ds-space margin="small">
<ds-number
:count="0"
:label="$t('admin.dashboard.shouts')"
size="x-large"
uppercase
>
<client-only slot="count">
<hc-count-to :end-val="data.statistics.countShouts" />
</client-only>
</ds-number>
</ds-space>
</ds-flex-item>
</ds-flex>
</ds-space>
</template>
</template>
</ApolloQuery>
</ds-card>
</template>
<script>
import gql from 'graphql-tag'
import HcCountTo from '~/components/CountTo.vue'
import { Statistics } from '~/graphql/admin/Statistics'
export default {
components: {
@ -92,30 +136,9 @@ export default {
},
data() {
return {
statistics: {},
errorIconPath: '/img/svg/emoji/cry.svg',
Statistics,
}
},
computed: {
isClient() {
return process.client
},
},
apollo: {
statistics: {
query: gql`
query {
statistics {
countUsers
countPosts
countComments
countNotifications
countInvites
countFollows
countShouts
}
}
`,
},
},
}
</script>

View File

@ -1,6 +1,6 @@
<template>
<ds-flex :width="{ base: '100%' }" gutter="base">
<ds-flex-item :width="{ base: '100%', md: 3 }">
<ds-flex-item :width="{ base: '100%', md: 5 }">
<hc-contribution-form />
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', md: 1 }">&nbsp;</ds-flex-item>

View File

@ -388,7 +388,7 @@ export default {
resetPostList() {
this.offset = 0
this.posts = []
this.hasMore = false
this.hasMore = true
},
async block(user) {
await this.$apollo.mutate({ mutation: Block(), variables: { id: user.id } })