Merge pull request #3140 from Human-Connection/17-Admin-Remove-user-profile

feat: 🍰 Admin - Remove User Profile
This commit is contained in:
Wolfgang Huß 2020-08-27 12:53:59 +02:00 committed by GitHub
commit 47e43f2f9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 542 additions and 305 deletions

View File

@ -6,8 +6,6 @@ import { createTestClient } from 'apollo-server-testing'
const categoryIds = ['cat9']
let user
let anotherUser
let moderator
let admin
let authenticatedUser
@ -18,6 +16,35 @@ let variables
const driver = getDriver()
const neode = getNeode()
const deleteUserMutation = gql`
mutation($id: ID!, $resource: [Deletable]) {
DeleteUser(id: $id, resource: $resource) {
id
name
about
deleted
contributions {
id
content
contentExcerpt
deleted
comments {
id
content
contentExcerpt
deleted
}
}
comments {
id
content
contentExcerpt
deleted
}
}
}
`
beforeAll(() => {
const { server } = createServer({
context: () => {
@ -51,7 +78,6 @@ describe('User', () => {
variables = {
email: 'any-email-address@example.org',
}
await Factory.build('user', { name: 'Johnny' }, { email: 'any-email-address@example.org' })
})
@ -248,38 +274,8 @@ describe('UpdateUser', () => {
})
})
describe('Delete a user', () => {
let deleteUserMutation
describe('Delete a User as admin', () => {
beforeEach(async () => {
deleteUserMutation = gql`
mutation($id: ID!, $resource: [Deletable]) {
DeleteUser(id: $id, resource: $resource) {
id
name
about
deleted
contributions {
id
content
contentExcerpt
deleted
comments {
id
content
contentExcerpt
deleted
}
}
comments {
id
content
contentExcerpt
deleted
}
}
}
`
variables = { id: ' u343', resource: [] }
user = await Factory.build('user', {
@ -289,112 +285,113 @@ describe('Delete a user', () => {
})
})
describe('as another user', () => {
describe('authenticated as Admin', () => {
beforeEach(async () => {
anotherUser = await Factory.build(
admin = await Factory.build(
'user',
{
role: 'user',
role: 'admin',
},
{
email: 'user@example.org',
email: 'admin@example.org',
password: '1234',
},
)
authenticatedUser = await anotherUser.toJson()
authenticatedUser = await admin.toJson()
})
it("an ordinary user has no authorization to delete another user's account", async () => {
const { errors } = await mutate({ mutation: deleteUserMutation, variables })
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
})
})
describe('as moderator', () => {
beforeEach(async () => {
moderator = await Factory.build(
'user',
{
role: 'moderator',
},
{
email: 'moderator@example.org',
password: '1234',
},
)
authenticatedUser = await moderator.toJson()
})
it('moderator is not allowed to delete other user accounts', async () => {
const { errors } = await mutate({ mutation: deleteUserMutation, variables })
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
})
})
describe('as admin', () => {
describe('authenticated as Admin', () => {
beforeEach(async () => {
admin = await Factory.build(
'user',
{
role: 'admin',
},
{
email: 'admin@example.org',
password: '1234',
},
)
authenticatedUser = await admin.toJson()
describe('deleting a user account', () => {
beforeEach(() => {
variables = { ...variables, id: 'u343' }
})
describe('deleting a user account', () => {
beforeEach(() => {
variables = { id: 'u343' }
describe('given posts and comments', () => {
beforeEach(async () => {
await Factory.build('category', {
id: 'cat9',
name: 'Democracy & Politics',
icon: 'university',
})
await Factory.build(
'post',
{
id: 'p139',
content: 'Post by user u343',
},
{
author: user,
categoryIds,
},
)
await Factory.build(
'comment',
{
id: 'c155',
content: 'Comment by user u343',
},
{
author: user,
},
)
await Factory.build(
'comment',
{
id: 'c156',
content: "A comment by someone else on user u343's post",
},
{
postId: 'p139',
},
)
})
describe('given posts and comments', () => {
beforeEach(async () => {
await Factory.build('category', {
id: 'cat9',
name: 'Democracy & Politics',
icon: 'university',
})
await Factory.build(
'post',
{
id: 'p139',
content: 'Post by user u343',
it("deletes account, but doesn't delete posts or comments by default", async () => {
const expectedResponse = {
data: {
DeleteUser: {
id: 'u343',
name: 'UNAVAILABLE',
about: 'UNAVAILABLE',
deleted: true,
contributions: [
{
id: 'p139',
content: 'Post by user u343',
contentExcerpt: 'Post by user u343',
deleted: false,
comments: [
{
id: 'c156',
content: "A comment by someone else on user u343's post",
contentExcerpt: "A comment by someone else on user u343's post",
deleted: false,
},
],
},
],
comments: [
{
id: 'c155',
content: 'Comment by user u343',
contentExcerpt: 'Comment by user u343',
deleted: false,
},
],
},
{
author: user,
categoryIds,
},
)
await Factory.build(
'comment',
{
id: 'c155',
content: 'Comment by user u343',
},
{
author: user,
},
)
await Factory.build(
'comment',
{
id: 'c156',
content: "A comment by someone else on user u343's post",
},
{
postId: 'p139',
},
)
},
errors: undefined,
}
await expect(mutate({ mutation: deleteUserMutation, variables })).resolves.toMatchObject(
expectedResponse,
)
})
describe('deletion of all posts and comments requested', () => {
beforeEach(() => {
variables = { ...variables, resource: ['Comment', 'Post'] }
})
it("deletes account, but doesn't delete posts or comments by default", async () => {
it('marks posts and comments as deleted', async () => {
const expectedResponse = {
data: {
DeleteUser: {
@ -405,15 +402,15 @@ describe('Delete a user', () => {
contributions: [
{
id: 'p139',
content: 'Post by user u343',
contentExcerpt: 'Post by user u343',
deleted: false,
content: 'UNAVAILABLE',
contentExcerpt: 'UNAVAILABLE',
deleted: true,
comments: [
{
id: 'c156',
content: "A comment by someone else on user u343's post",
contentExcerpt: "A comment by someone else on user u343's post",
deleted: false,
content: 'UNAVAILABLE',
contentExcerpt: 'UNAVAILABLE',
deleted: true,
},
],
},
@ -421,9 +418,9 @@ describe('Delete a user', () => {
comments: [
{
id: 'c155',
content: 'Comment by user u343',
contentExcerpt: 'Comment by user u343',
deleted: false,
content: 'UNAVAILABLE',
contentExcerpt: 'UNAVAILABLE',
deleted: true,
},
],
},
@ -434,175 +431,28 @@ describe('Delete a user', () => {
mutate({ mutation: deleteUserMutation, variables }),
).resolves.toMatchObject(expectedResponse)
})
})
})
describe('deletion of all post requested', () => {
beforeEach(() => {
variables = { ...variables, resource: ['Post'] }
})
describe('connected `EmailAddress` nodes', () => {
it('will be removed completely', async () => {
await expect(neode.all('EmailAddress')).resolves.toHaveLength(2)
await mutate({ mutation: deleteUserMutation, variables })
it('on request', async () => {
const expectedResponse = {
data: {
DeleteUser: {
id: 'u343',
name: 'UNAVAILABLE',
about: 'UNAVAILABLE',
deleted: true,
contributions: [
{
id: 'p139',
content: 'UNAVAILABLE',
contentExcerpt: 'UNAVAILABLE',
deleted: true,
comments: [
{
id: 'c156',
content: 'UNAVAILABLE',
contentExcerpt: 'UNAVAILABLE',
deleted: true,
},
],
},
],
comments: [
{
id: 'c155',
content: 'Comment by user u343',
contentExcerpt: 'Comment by user u343',
deleted: false,
},
],
},
},
errors: undefined,
}
await expect(
mutate({ mutation: deleteUserMutation, variables }),
).resolves.toMatchObject(expectedResponse)
})
await expect(neode.all('EmailAddress')).resolves.toHaveLength(1)
})
})
it('deletes user avatar and post hero images', async () => {
await expect(neode.all('Image')).resolves.toHaveLength(22)
await mutate({ mutation: deleteUserMutation, variables })
await expect(neode.all('Image')).resolves.toHaveLength(20)
})
})
describe('deletion of all comments requested', () => {
beforeEach(() => {
variables = { ...variables, resource: ['Comment'] }
})
it('marks comments as deleted', async () => {
const expectedResponse = {
data: {
DeleteUser: {
id: 'u343',
name: 'UNAVAILABLE',
about: 'UNAVAILABLE',
deleted: true,
contributions: [
{
id: 'p139',
content: 'Post by user u343',
contentExcerpt: 'Post by user u343',
deleted: false,
comments: [
{
id: 'c156',
content: "A comment by someone else on user u343's post",
contentExcerpt: "A comment by someone else on user u343's post",
deleted: false,
},
],
},
],
comments: [
{
id: 'c155',
content: 'UNAVAILABLE',
contentExcerpt: 'UNAVAILABLE',
deleted: true,
},
],
},
},
errors: undefined,
}
await expect(
mutate({ mutation: deleteUserMutation, variables }),
).resolves.toMatchObject(expectedResponse)
})
})
describe('deletion of all posts and comments requested', () => {
beforeEach(() => {
variables = { ...variables, resource: ['Comment', 'Post'] }
})
it('marks posts and comments as deleted', async () => {
const expectedResponse = {
data: {
DeleteUser: {
id: 'u343',
name: 'UNAVAILABLE',
about: 'UNAVAILABLE',
deleted: true,
contributions: [
{
id: 'p139',
content: 'UNAVAILABLE',
contentExcerpt: 'UNAVAILABLE',
deleted: true,
comments: [
{
id: 'c156',
content: 'UNAVAILABLE',
contentExcerpt: 'UNAVAILABLE',
deleted: true,
},
],
},
],
comments: [
{
id: 'c155',
content: 'UNAVAILABLE',
contentExcerpt: 'UNAVAILABLE',
deleted: true,
},
],
},
},
errors: undefined,
}
await expect(
mutate({ mutation: deleteUserMutation, variables }),
).resolves.toMatchObject(expectedResponse)
})
})
describe('connected `SocialMedia` nodes', () => {
beforeEach(async () => {
const socialMedia = await Factory.build('socialMedia')
await socialMedia.relateTo(user, 'ownedBy')
})
describe('connected `EmailAddress` nodes', () => {
it('will be removed completely', async () => {
await expect(neode.all('EmailAddress')).resolves.toHaveLength(2)
await mutate({ mutation: deleteUserMutation, variables })
await expect(neode.all('EmailAddress')).resolves.toHaveLength(1)
})
})
describe('connected `SocialMedia` nodes', () => {
beforeEach(async () => {
const socialMedia = await Factory.build('socialMedia')
await socialMedia.relateTo(user, 'ownedBy')
})
it('will be removed completely', async () => {
await expect(neode.all('SocialMedia')).resolves.toHaveLength(1)
await mutate({ mutation: deleteUserMutation, variables })
await expect(neode.all('SocialMedia')).resolves.toHaveLength(0)
})
it('will be removed completely', async () => {
await expect(neode.all('SocialMedia')).resolves.toHaveLength(1)
await mutate({ mutation: deleteUserMutation, variables })
await expect(neode.all('SocialMedia')).resolves.toHaveLength(0)
})
})
})

View File

@ -51,8 +51,8 @@ const newlyCreatedNodesWithLocales = [
country: {
id: expect.stringContaining('country'),
type: 'country',
name: 'United States of America',
nameEN: 'United States of America',
name: 'United States',
nameEN: 'United States',
nameDE: 'Vereinigte Staaten',
namePT: 'Estados Unidos',
nameES: 'Estados Unidos',

View File

@ -12,11 +12,6 @@ type Mutation {
unfollowUser(id: ID!): User
}
enum Deletable {
Post
Comment
}
enum ShoutTypeEnum {
Post
}

View File

@ -185,6 +185,11 @@ type Query {
)
}
enum Deletable {
Post
Comment
}
type Mutation {
UpdateUser (
id: ID!

View File

@ -134,6 +134,47 @@ describe('ContentMenu.vue', () => {
],
])
})
it('can delete another user', () => {
getters['auth/user'] = () => {
return { id: 'some-user', slug: 'some-user' }
}
const wrapper = openContentMenu({
resourceType: 'user',
resource: {
id: 'another-user',
slug: 'another-user',
},
})
wrapper
.findAll('.ds-menu-item')
.filter((item) => item.text() === 'settings.deleteUserAccount.name')
.at(0)
.trigger('click')
expect(wrapper.emitted('delete')).toEqual([
[
{
id: 'another-user',
slug: 'another-user',
},
],
])
})
it('can not delete the own account', () => {
const wrapper = openContentMenu({
resourceType: 'user',
resource: {
id: 'some-user',
slug: 'some-user',
},
})
expect(
wrapper
.findAll('.ds-menu-item')
.filter((item) => item.text() === 'settings.deleteUserAccount.name'),
).toEqual({})
})
})
describe('owner of comment can', () => {

View File

@ -191,6 +191,15 @@ export default {
icon: 'user-times',
})
}
if (this.isAdmin === true) {
routes.push({
label: this.$t(`settings.deleteUserAccount.name`),
callback: () => {
this.$emit('delete', this.resource)
},
icon: 'trash',
})
}
}
}

View File

@ -50,12 +50,20 @@ describe('DeleteData.vue', () => {
})
describe('mount', () => {
const data = () => {
return {
currentUserCounts: {
contributionsCount: 4,
commentedCount: 2,
},
}
}
const Wrapper = () => {
const store = new Vuex.Store({
getters,
actions,
})
return mount(DeleteData, { mocks, localVue, store })
return mount(DeleteData, { mocks, localVue, store, data })
}
beforeEach(() => {

View File

@ -8,7 +8,9 @@
{{ $t('settings.deleteUserAccount.pleaseConfirm', { confirm: currentUser.name }) }}
</label>
<ds-input v-model="enableDeletionValue" />
<p class="notice">{{ $t('settings.deleteUserAccount.accountDescription') }}</p>
<p v-show="enableDeletionValue" class="notice">
{{ $t('settings.deleteUserAccount.accountDescription') }}
</p>
<label class="checkbox">
<input
type="checkbox"

View File

@ -30,6 +30,7 @@
:modalData="data.modalData"
@close="close"
/>
<delete-user-modal v-if="open === 'delete'" :userdata="data.userdata" @close="close" />
</div>
</template>
@ -38,6 +39,7 @@ import ConfirmModal from '~/components/Modal/ConfirmModal'
import DisableModal from '~/components/Modal/DisableModal'
import ReleaseModal from '~/components/ReleaseModal/ReleaseModal.vue'
import ReportModal from '~/components/Modal/ReportModal'
import DeleteUserModal from '~/components/Modal/DeleteUserModal.vue'
import { mapGetters } from 'vuex'
export default {
@ -47,6 +49,7 @@ export default {
ReleaseModal,
ReportModal,
ConfirmModal,
DeleteUserModal,
},
computed: {
...mapGetters({

View File

@ -0,0 +1,115 @@
import { config, mount, shallowMount } from '@vue/test-utils'
import Vuex from 'vuex'
import DeleteUserModal from './DeleteUserModal.vue'
const localVue = global.localVue
config.stubs['sweetalert-icon'] = '<span><slot /></span>'
config.stubs['nuxt-link'] = '<span><slot /></span>'
localVue.use(DeleteUserModal)
const getters = {
'auth/isAdmin': () => true,
'auth/isModerator': () => false,
}
describe('DeleteUserModal.vue', () => {
const store = new Vuex.Store({ getters })
let wrapper
let propsData = {
userdata: {
name: 'another-user',
slug: 'another-user',
createdAt: '2020-08-12T08:34:05.803Z',
contributionsCount: '4',
commentedCount: '2',
},
}
const mocks = {
$t: jest.fn(),
$filters: {
truncate: (a) => a,
},
$toast: {
success: jest.fn(),
error: jest.fn(),
},
$i18n: {
locale: () => 'en',
},
}
afterEach(() => {
jest.clearAllMocks()
})
describe('shallowMount', () => {
const Wrapper = () => {
return shallowMount(DeleteUserModal, {
propsData,
mocks,
store,
localVue,
})
}
describe('defaults', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('success false', () => {
expect(wrapper.vm.success).toBe(false)
})
it('loading false', () => {
expect(wrapper.vm.loading).toBe(false)
})
})
})
describe('mount', () => {
const Wrapper = () => {
return mount(DeleteUserModal, {
propsData,
mocks,
store,
localVue,
})
}
beforeEach(jest.useFakeTimers)
describe('given another user', () => {
beforeEach(() => {
propsData = {
...propsData,
type: 'user',
id: 'u4711',
}
wrapper = Wrapper()
})
describe('click cancel button', () => {
beforeEach(() => {
wrapper = Wrapper()
wrapper.find('button.cancel').trigger('click')
})
it('does not emit "close" yet', () => {
expect(wrapper.emitted().close).toBeFalsy()
})
it('fades away', () => {
expect(wrapper.vm.isOpen).toBe(false)
})
describe('after timeout', () => {
beforeEach(jest.runAllTimers)
it('emits "close"', () => {
expect(wrapper.emitted().close).toBeTruthy()
})
})
})
})
})
})

View File

@ -0,0 +1,182 @@
<template>
<ds-modal class="delete-user-modal" :title="title" :is-open="isOpen" @cancel="cancel">
<transition name="ds-transition-fade">
<ds-flex v-if="success" class="hc-modal-success" centered>
<sweetalert-icon icon="success" />
</ds-flex>
</transition>
<div>
<ds-section>
<ds-flex>
<ds-flex-item width="50%">
<user-teaser :user="userdata" />
</ds-flex-item>
<ds-flex-item width="20%">
<ds-text size="small">
<span class="bold">{{ $t('modals.deleteUser.created') }}</span>
<br />
<relative-date-time :date-time="userdata.createdAt" />
</ds-text>
</ds-flex-item>
<ds-flex-item width="15%">
<ds-text size="small">
<span class="bold">{{ $t('common.post', null, userdata.contributionsCount) }}</span>
<br />
{{ userdata.contributionsCount }}
</ds-text>
</ds-flex-item>
<ds-flex-item width="15%">
<ds-text size="small">
<span class="bold">{{ $t('common.comment', null, userdata.commentedCount) }}</span>
<br />
{{ userdata.commentedCount }}
</ds-text>
</ds-flex-item>
</ds-flex>
</ds-section>
</div>
<template slot="footer">
<base-button class="cancel" @click="cancel">{{ $t('actions.cancel') }}</base-button>
<base-button danger filled class="confirm" icon="exclamation-circle" @click="openModal">
{{ $t('settings.deleteUserAccount.name') }}
</base-button>
</template>
</ds-modal>
</template>
<script>
import gql from 'graphql-tag'
import { mapMutations } from 'vuex'
import { SweetalertIcon } from 'vue-sweetalert-icons'
import RelativeDateTime from '~/components/RelativeDateTime'
import UserTeaser from '~/components/UserTeaser/UserTeaser'
export default {
name: 'DeleteUserModal',
components: {
UserTeaser,
SweetalertIcon,
RelativeDateTime,
},
props: {
userdata: { type: Object, required: true },
},
data() {
return {
isOpen: true,
success: false,
loading: false,
// isAdmin: this.$store.getters['auth/isAdmin'],
isAdmin: true,
}
},
computed: {
title() {
return this.$t('settings.deleteUserAccount.name')
},
modalData(userdata) {
return function (userdata) {
return {
name: 'confirm',
data: {
type: userdata.name,
resource: userdata,
modalData: {
titleIdent: this.$t('settings.deleteUserAccount.accountWarningIsAdmin'),
messageIdent: this.$t('settings.deleteUserAccount.infoAdmin'),
messageParams: {},
buttons: {
confirm: {
danger: true,
icon: 'trash',
textIdent: this.$t('settings.deleteUserAccount.confirmDeleting'),
callback: () => {
this.confirm(userdata)
},
},
cancel: {
icon: 'close',
textIdent: this.$t('actions.cancel'),
callback: () => {},
},
},
},
},
}
}
},
},
methods: {
...mapMutations({
commitModalData: 'modal/SET_OPEN',
}),
openModal() {
this.commitModalData(this.modalData(this.userdata))
},
cancel() {
// TODO: Use the "modalData" structure introduced in "ConfirmModal" and refactor this here. Be aware that all the Jest tests have to be refactored as well !!!
// await this.modalData.buttons.cancel.callback()
this.isOpen = false
setTimeout(() => {
this.$emit('close')
}, 1000)
},
async confirm() {
this.$apollo
.mutate({
mutation: gql`
mutation($id: ID!, $resource: [Deletable]) {
DeleteUser(id: $id, resource: $resource) {
id
}
}
`,
variables: { id: this.userdata.id, resource: ['Post', 'Comment'] },
})
.then(({ _data }) => {
this.success = true
this.$toast.success(this.$t('settings.deleteUserAccount.success'))
setTimeout(() => {
this.isOpen = false
setTimeout(() => {
this.success = false
this.$emit('close')
this.$router.history.replace('/')
}, 500)
}, 1500)
this.loading = false
})
.catch((err) => {
this.$emit('close')
this.success = false
this.$toast.error(err.message)
this.isOpen = false
this.loading = false
})
},
},
}
</script>
<style lang="scss">
.ds-modal {
max-width: 700px !important;
}
.hc-modal-success {
pointer-events: none;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
background-color: #fff;
opacity: 1;
z-index: $z-index-modal;
border-radius: $border-radius-x-large;
}
.bold {
font-weight: 700;
}
</style>

View File

@ -373,6 +373,11 @@
"questions": "Bei Fragen oder Problemen erreichst Du uns per E-Mail an",
"title": "Human Connection befindet sich in der Wartung"
},
"modals": {
"deleteUser": {
"created": "Erstellt"
}
},
"moderation": {
"name": "Moderation",
"reports": {
@ -632,8 +637,12 @@
"deleteUserAccount": {
"accountDescription": "Sei dir bewusst, dass deine Beiträge und Kommentare für unsere Community wichtig sind. Wenn du sie trotzdem löschen möchtest, musst du sie unten markieren.",
"accountWarning": "Dein Konto, deine Beiträge oder Kommentare kannst du nach dem Löschen WEDER VERWALTEN NOCH WIEDERHERSTELLEN!",
"accountWarningAdmin": "Das Konto, die Beiträge oder Kommentare können nach dem Löschen WEDER VERWALTET NOCH WIEDERHERGESTELLT WERDEN!",
"accountWarningIsAdmin": "Achtung! Du löschst jetzt ein Benutzerkonto!",
"commentedCount": "Meinen {count} Kommentar löschen ::: Meine {count} Kommentare löschen",
"confirmDeleting": "Benutzerkonto jetzt löschen",
"contributionsCount": "Meinen {count} Beitrag löschen ::: Meine {count} Beiträge löschen",
"infoAdmin": "Alle Beiträge und Kommentare des Users werden zusätzlich gelöscht!",
"name": "Benutzerkonto löschen",
"pleaseConfirm": "Zerstörerische Aktion! Gib „{confirm}“ ein, um zu bestätigen.",
"success": "Konto erfolgreich gelöscht!"

View File

@ -373,6 +373,11 @@
"questions": "Any Questions or concerns, send an e-mail to",
"title": "Human Connection is under maintenance"
},
"modals": {
"deleteUser": {
"created": "Created"
}
},
"moderation": {
"name": "Moderation",
"reports": {
@ -632,8 +637,12 @@
"deleteUserAccount": {
"accountDescription": "Be aware that your Posts and Comments are important to our community. If you still choose to delete them, you have to mark them below.",
"accountWarning": "You CAN'T MANAGE and CAN'T RECOVER your Account, Posts, or Comments after deleting your account!",
"accountWarningAdmin": "The account, contributions or comments can NOT BE ADMINISTERED OR RESTORED after deletion!",
"accountWarningIsAdmin": "Attention! You are about to delete a user account!",
"commentedCount": "Delete my {count} comment ::: Delete my {count} comments",
"confirmDeleting": "Delete user account now",
"contributionsCount": "Delete my {count} post ::: Delete my {count} posts",
"infoAdmin": "All contributions and comments of the user will be deleted additionally!",
"name": "Delete user account",
"pleaseConfirm": "Destructive action! Type “{confirm}” to confirm.",
"success": "Account successfully deleted!"

View File

@ -23,6 +23,7 @@
@unmute="unmuteUser"
@block="blockUser"
@unblock="unblockUser"
@delete="deleteUser"
/>
</client-only>
<ds-space margin="small">
@ -385,6 +386,14 @@ export default {
this.$apollo.queries.User.refetch()
}
},
async deleteUser(userdata) {
this.$store.commit('modal/SET_OPEN', {
name: 'delete',
data: {
userdata: userdata,
},
})
},
pinPost(post) {
this.$apollo
.mutate({