mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
Merge branch 'master' of https://github.com/Human-Connection/Human-Connection into 1505_remove_html_in_moderation_view
This commit is contained in:
commit
40103e84e7
@ -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
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -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")
|
||||
|
||||
14
backend/src/schema/types/type/Statistics.gql
Normal file
14
backend/src/schema/types/type/Statistics.gql
Normal 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!
|
||||
}
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
15
webapp/graphql/admin/Statistics.js
Normal file
15
webapp/graphql/admin/Statistics.js
Normal file
@ -0,0 +1,15 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const Statistics = gql`
|
||||
query {
|
||||
statistics {
|
||||
countUsers
|
||||
countPosts
|
||||
countComments
|
||||
countNotifications
|
||||
countInvites
|
||||
countFollows
|
||||
countShouts
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -22,6 +22,7 @@
|
||||
},
|
||||
"site": {
|
||||
"thanks": "Danke!",
|
||||
"error-occurred": "Ein Fehler ist aufgetreten.",
|
||||
"made": "Mit ❤ gemacht",
|
||||
"imprint": "Impressum",
|
||||
"data-privacy": "Datenschutz",
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
},
|
||||
"site": {
|
||||
"thanks": "Thanks!",
|
||||
"error-occurred": "An error occurred.",
|
||||
"made": "Made with ❤",
|
||||
"imprint": "Imprint",
|
||||
"termsAndConditions": "Terms and conditions",
|
||||
|
||||
53
webapp/pages/admin/index.spec.js
Normal file
53
webapp/pages/admin/index.spec.js
Normal 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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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>
|
||||
|
||||
@ -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 }"> </ds-flex-item>
|
||||
|
||||
@ -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 } })
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user