diff --git a/assets/styles/main.scss b/assets/styles/main.scss index 662b8bb0d..db967e973 100644 --- a/assets/styles/main.scss +++ b/assets/styles/main.scss @@ -4,6 +4,22 @@ // Transition Easing $easeOut: cubic-bezier(0.19, 1, 0.22, 1); +.disabled-content { + position: relative; + + &::before { + @include border-radius($border-radius-x-large); + box-shadow: inset 0 0 0 5px $color-danger; + content: ""; + display: block; + position: absolute; + width: 100%; + height: 100%; + z-index: 2; + pointer-events: none; + } +} + .layout-enter-active { transition: opacity 80ms ease-out; transition-delay: 80ms; diff --git a/components/Author.vue b/components/Author.vue deleted file mode 100644 index 54770d33d..000000000 --- a/components/Author.vue +++ /dev/null @@ -1,184 +0,0 @@ - - - - - diff --git a/components/Comment.spec.js b/components/Comment.spec.js new file mode 100644 index 000000000..83a738956 --- /dev/null +++ b/components/Comment.spec.js @@ -0,0 +1,95 @@ +import { config, shallowMount, mount, createLocalVue } from '@vue/test-utils' +import Comment from './Comment.vue' +import Vue from 'vue' +import Vuex from 'vuex' +import Styleguide from '@human-connection/styleguide' + +const localVue = createLocalVue() + +localVue.use(Vuex) +localVue.use(Styleguide) + +config.stubs['no-ssr'] = '' + +describe('Comment.vue', () => { + let wrapper + let Wrapper + let propsData + let mocks + let getters + + beforeEach(() => { + propsData = {} + mocks = { + $t: jest.fn() + } + getters = { + 'auth/user': () => { + return {} + }, + 'auth/isModerator': () => false + } + }) + + describe('shallowMount', () => { + const Wrapper = () => { + const store = new Vuex.Store({ + getters + }) + return shallowMount(Comment, { store, propsData, mocks, localVue }) + } + + describe('given a comment', () => { + beforeEach(() => { + propsData.comment = { + id: '2', + contentExcerpt: 'Hello I am a comment content' + } + }) + + it('renders content', () => { + const wrapper = Wrapper() + expect(wrapper.text()).toMatch('Hello I am a comment content') + }) + + describe('which is disabled', () => { + beforeEach(() => { + propsData.comment.disabled = true + }) + + it('renders no comment data', () => { + const wrapper = Wrapper() + expect(wrapper.text()).not.toMatch('comment content') + }) + + it('has no "disabled-content" css class', () => { + const wrapper = Wrapper() + expect(wrapper.classes()).not.toContain('disabled-content') + }) + + it('translates a placeholder', () => { + const wrapper = Wrapper() + const calls = mocks.$t.mock.calls + const expected = [['comment.content.unavailable-placeholder']] + expect(calls).toEqual(expect.arrayContaining(expected)) + }) + + describe('for a moderator', () => { + beforeEach(() => { + getters['auth/isModerator'] = () => true + }) + + it('renders comment data', () => { + const wrapper = Wrapper() + expect(wrapper.text()).toMatch('comment content') + }) + + it('has a "disabled-content" css class', () => { + const wrapper = Wrapper() + expect(wrapper.classes()).toContain('disabled-content') + }) + }) + }) + }) + }) +}) diff --git a/components/Comment.vue b/components/Comment.vue new file mode 100644 index 000000000..71043e90d --- /dev/null +++ b/components/Comment.vue @@ -0,0 +1,76 @@ + + + diff --git a/components/ContentMenu.vue b/components/ContentMenu.vue index 5e614c476..2a63bb99b 100644 --- a/components/ContentMenu.vue +++ b/components/ContentMenu.vue @@ -52,10 +52,9 @@ export default { }, props: { placement: { type: String, default: 'top-end' }, - itemId: { type: String, required: true }, - name: { type: String, required: true }, + resource: { type: Object, required: true }, isOwner: { type: Boolean, default: false }, - context: { + resourceType: { type: String, required: true, validator: value => { @@ -67,19 +66,19 @@ export default { routes() { let routes = [] - if (this.isOwner && this.context === 'contribution') { + if (this.isOwner && this.resourceType === 'contribution') { routes.push({ name: this.$t(`contribution.edit`), path: this.$router.resolve({ name: 'post-edit-id', params: { - id: this.itemId + id: this.resource.id } }).href, icon: 'edit' }) } - if (this.isOwner && this.context === 'comment') { + if (this.isOwner && this.resourceType === 'comment') { routes.push({ name: this.$t(`comment.edit`), callback: () => { @@ -91,21 +90,25 @@ export default { if (!this.isOwner) { routes.push({ - name: this.$t(`report.${this.context}.title`), - callback: this.openReportDialog, + name: this.$t(`report.${this.resourceType}.title`), + callback: () => { + this.openModal('report') + }, icon: 'flag' }) } if (!this.isOwner && this.isModerator) { routes.push({ - name: this.$t(`disable.${this.context}.title`), - callback: () => {}, + name: this.$t(`disable.${this.resourceType}.title`), + callback: () => { + this.openModal('disable') + }, icon: 'eye-slash' }) } - if (this.isOwner && this.context === 'user') { + if (this.isOwner && this.resourceType === 'user') { routes.push({ name: this.$t(`settings.data.name`), // eslint-disable-next-line vue/no-side-effects-in-computed-properties @@ -128,18 +131,14 @@ export default { } toggleMenu() }, - openReportDialog() { + openModal(dialog) { this.$store.commit('modal/SET_OPEN', { - name: 'report', + name: dialog, data: { - context: this.context, - id: this.itemId, - name: this.name + type: this.resourceType, + resource: this.resource } }) - }, - openDisableDialog() { - this.$toast.error('NOT IMPLEMENTED!') } } } diff --git a/components/Modal.spec.js b/components/Modal.spec.js new file mode 100644 index 000000000..1ec032edb --- /dev/null +++ b/components/Modal.spec.js @@ -0,0 +1,124 @@ +import { shallowMount, mount, createLocalVue } from '@vue/test-utils' +import Modal from './Modal.vue' +import DisableModal from './Modal/DisableModal.vue' +import ReportModal from './Modal/ReportModal.vue' +import Vue from 'vue' +import Vuex from 'vuex' +import { getters, mutations } from '../store/modal' +import Styleguide from '@human-connection/styleguide' + +const localVue = createLocalVue() + +localVue.use(Vuex) +localVue.use(Styleguide) + +describe('Modal.vue', () => { + let Wrapper + let wrapper + let store + let state + let mocks + + const createWrapper = mountMethod => { + return () => { + store = new Vuex.Store({ + state, + getters: { + 'modal/open': getters.open, + 'modal/data': getters.data + }, + mutations: { + 'modal/SET_OPEN': mutations.SET_OPEN + } + }) + return mountMethod(Modal, { store, mocks, localVue }) + } + } + + beforeEach(() => { + mocks = { + $filters: { + truncate: a => a + }, + $toast: { + success: () => {}, + error: () => {} + }, + $t: () => {} + } + state = { + open: null, + data: {} + } + }) + + describe('shallowMount', () => { + const Wrapper = createWrapper(shallowMount) + + it('initially empty', () => { + wrapper = Wrapper() + expect(wrapper.contains(DisableModal)).toBe(false) + expect(wrapper.contains(ReportModal)).toBe(false) + }) + + describe('store/modal holds data to disable', () => { + beforeEach(() => { + state = { + open: 'disable', + data: { + type: 'contribution', + resource: { + id: 'c456', + title: 'some title' + } + } + } + wrapper = Wrapper() + }) + + it('renders disable modal', () => { + expect(wrapper.contains(DisableModal)).toBe(true) + }) + + it('passes data to disable modal', () => { + expect(wrapper.find(DisableModal).props()).toEqual({ + type: 'contribution', + name: 'some title', + id: 'c456' + }) + }) + + describe('child component emits close', () => { + it('turns empty', () => { + wrapper.find(DisableModal).vm.$emit('close') + expect(wrapper.contains(DisableModal)).toBe(false) + }) + }) + + describe('store/modal data contains a comment', () => { + it('passes author name to disable modal', () => { + state.data = { + type: 'comment', + resource: { id: 'c456', author: { name: 'Author name' } } + } + wrapper = Wrapper() + expect(wrapper.find(DisableModal).props()).toEqual({ + type: 'comment', + name: 'Author name', + id: 'c456' + }) + }) + + it('does not crash if author is undefined', () => { + state.data = { type: 'comment', resource: { id: 'c456' } } + wrapper = Wrapper() + expect(wrapper.find(DisableModal).props()).toEqual({ + type: 'comment', + name: '', + id: 'c456' + }) + }) + }) + }) + }) +}) diff --git a/components/Modal.vue b/components/Modal.vue new file mode 100644 index 000000000..0507e5439 --- /dev/null +++ b/components/Modal.vue @@ -0,0 +1,59 @@ + + + diff --git a/components/Modal/DisableModal.spec.js b/components/Modal/DisableModal.spec.js new file mode 100644 index 000000000..e4debdc70 --- /dev/null +++ b/components/Modal/DisableModal.spec.js @@ -0,0 +1,150 @@ +import { shallowMount, mount, createLocalVue } from '@vue/test-utils' +import DisableModal from './DisableModal.vue' +import Vue from 'vue' +import Styleguide from '@human-connection/styleguide' + +const localVue = createLocalVue() + +localVue.use(Styleguide) + +describe('DisableModal.vue', () => { + let store + let mocks + let propsData + let wrapper + + beforeEach(() => { + propsData = { + type: 'contribution', + name: 'blah', + id: 'c42' + } + mocks = { + $filters: { + truncate: a => a + }, + $toast: { + success: () => {}, + error: () => {} + }, + $t: jest.fn(), + $apollo: { + mutate: jest.fn().mockResolvedValue() + } + } + }) + + describe('shallowMount', () => { + const Wrapper = () => { + return shallowMount(DisableModal, { propsData, mocks, localVue }) + } + + describe('given a user', () => { + beforeEach(() => { + propsData = { + type: 'user', + id: 'u2', + name: 'Bob Ross' + } + }) + + it('mentions user name', () => { + Wrapper() + const calls = mocks.$t.mock.calls + const expected = [['disable.user.message', { name: 'Bob Ross' }]] + expect(calls).toEqual(expect.arrayContaining(expected)) + }) + }) + + describe('given a contribution', () => { + beforeEach(() => { + propsData = { + type: 'contribution', + id: 'c3', + name: 'This is some post title.' + } + }) + + it('mentions contribution title', () => { + Wrapper() + const calls = mocks.$t.mock.calls + const expected = [ + ['disable.contribution.message', { name: 'This is some post title.' }] + ] + expect(calls).toEqual(expect.arrayContaining(expected)) + }) + }) + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(DisableModal, { propsData, mocks, localVue }) + } + beforeEach(jest.useFakeTimers) + + describe('given id', () => { + beforeEach(() => { + propsData = { + type: 'user', + id: 'u4711' + } + }) + + describe('click cancel button', () => { + beforeEach(async () => { + wrapper = Wrapper() + await 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('does not call mutation', () => { + expect(mocks.$apollo.mutate).not.toHaveBeenCalled() + }) + + it('emits close', () => { + expect(wrapper.emitted().close).toBeTruthy() + }) + }) + }) + + describe('click confirm button', () => { + beforeEach(async () => { + wrapper = Wrapper() + await wrapper.find('button.confirm').trigger('click') + }) + + it('calls mutation', () => { + expect(mocks.$apollo.mutate).toHaveBeenCalled() + }) + + it('passes id to mutation', () => { + const calls = mocks.$apollo.mutate.mock.calls + const [[{ variables }]] = calls + expect(variables).toEqual({ id: 'u4711' }) + }) + + it('fades away', () => { + expect(wrapper.vm.isOpen).toBe(false) + }) + + describe('after timeout', () => { + beforeEach(jest.runAllTimers) + + it('emits close', () => { + expect(wrapper.emitted().close).toBeTruthy() + }) + }) + }) + }) + }) +}) diff --git a/components/Modal/DisableModal.vue b/components/Modal/DisableModal.vue new file mode 100644 index 000000000..4ab0293cd --- /dev/null +++ b/components/Modal/DisableModal.vue @@ -0,0 +1,83 @@ + + + diff --git a/components/Modal/ReportModal.spec.js b/components/Modal/ReportModal.spec.js new file mode 100644 index 000000000..865348512 --- /dev/null +++ b/components/Modal/ReportModal.spec.js @@ -0,0 +1,168 @@ +import { shallowMount, mount, createLocalVue } from '@vue/test-utils' +import ReportModal from './ReportModal.vue' +import Vue from 'vue' +import Vuex from 'vuex' +import Styleguide from '@human-connection/styleguide' + +const localVue = createLocalVue() + +localVue.use(Vuex) +localVue.use(Styleguide) + +describe('ReportModal.vue', () => { + let wrapper + let Wrapper + let propsData + let mocks + + beforeEach(() => { + propsData = { + type: 'contribution', + id: 'c43' + } + mocks = { + $t: jest.fn(), + $filters: { + truncate: a => a + }, + $toast: { + success: () => {}, + error: () => {} + }, + $apollo: { + mutate: jest.fn().mockResolvedValue() + } + } + }) + + describe('shallowMount', () => { + const Wrapper = () => { + return shallowMount(ReportModal, { propsData, mocks, localVue }) + } + + describe('defaults', () => { + it('success false', () => { + expect(Wrapper().vm.success).toBe(false) + }) + + it('loading false', () => { + expect(Wrapper().vm.loading).toBe(false) + }) + }) + + describe('given a user', () => { + beforeEach(() => { + propsData = { + type: 'user', + id: 'u4', + name: 'Bob Ross' + } + }) + + it('mentions user name', () => { + Wrapper() + const calls = mocks.$t.mock.calls + const expected = [['report.user.message', { name: 'Bob Ross' }]] + expect(calls).toEqual(expect.arrayContaining(expected)) + }) + }) + + describe('given a post', () => { + beforeEach(() => { + propsData = { + id: 'p23', + type: 'post', + name: 'It is a post' + } + }) + + it('mentions post title', () => { + Wrapper() + const calls = mocks.$t.mock.calls + const expected = [['report.post.message', { name: 'It is a post' }]] + expect(calls).toEqual(expect.arrayContaining(expected)) + }) + }) + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(ReportModal, { propsData, mocks, localVue }) + } + + beforeEach(jest.useFakeTimers) + + it('renders', () => { + expect(Wrapper().is('div')).toBe(true) + }) + + describe('given id', () => { + beforeEach(() => { + propsData = { + type: 'user', + id: 'u4711' + } + wrapper = Wrapper() + }) + + describe('click cancel button', () => { + beforeEach(() => { + wrapper = Wrapper() + wrapper.find('button.cancel').trigger('click') + }) + + describe('after timeout', () => { + beforeEach(jest.runAllTimers) + + it('fades away', () => { + expect(wrapper.vm.isOpen).toBe(false) + }) + + it('emits "close"', () => { + expect(wrapper.emitted().close).toBeTruthy() + }) + + it('does not call mutation', () => { + expect(mocks.$apollo.mutate).not.toHaveBeenCalled() + }) + }) + }) + + describe('click confirm button', () => { + beforeEach(() => { + wrapper.find('button.confirm').trigger('click') + }) + + it('calls report mutation', () => { + expect(mocks.$apollo.mutate).toHaveBeenCalled() + }) + + it('sets success', () => { + expect(wrapper.vm.success).toBe(true) + }) + + it('displays a success message', () => { + const calls = mocks.$t.mock.calls + const expected = [['report.success']] + expect(calls).toEqual(expect.arrayContaining(expected)) + }) + + describe('after timeout', () => { + beforeEach(jest.runAllTimers) + + it('fades away', () => { + expect(wrapper.vm.isOpen).toBe(false) + }) + + it('emits close', () => { + expect(wrapper.emitted().close).toBeTruthy() + }) + + it('resets success', () => { + expect(wrapper.vm.success).toBe(false) + }) + }) + }) + }) + }) +}) diff --git a/components/Modal/ReportModal.vue b/components/Modal/ReportModal.vue new file mode 100644 index 000000000..846fe5420 --- /dev/null +++ b/components/Modal/ReportModal.vue @@ -0,0 +1,127 @@ + + + + + diff --git a/components/PostCard.vue b/components/PostCard.vue index 090517a37..8f534f6ff 100644 --- a/components/PostCard.vue +++ b/components/PostCard.vue @@ -2,7 +2,7 @@ + + + {{ post.createdAt | dateTime('dd. MMMM yyyy HH:mm') }} + + - @@ -52,9 +61,8 @@ @@ -64,24 +72,20 @@ diff --git a/components/ReportModal.vue b/components/ReportModal.vue deleted file mode 100644 index 5e374e346..000000000 --- a/components/ReportModal.vue +++ /dev/null @@ -1,144 +0,0 @@ - - - - - diff --git a/components/User.spec.js b/components/User.spec.js new file mode 100644 index 000000000..0bbf13529 --- /dev/null +++ b/components/User.spec.js @@ -0,0 +1,102 @@ +import { config, mount, createLocalVue, RouterLinkStub } from '@vue/test-utils' +import User from './User.vue' +import Vue from 'vue' +import Vuex from 'vuex' +import VTooltip from 'v-tooltip' + +import Styleguide from '@human-connection/styleguide' + +const localVue = createLocalVue() +const filter = jest.fn(str => str) + +localVue.use(Vuex) +localVue.use(VTooltip) +localVue.use(Styleguide) + +localVue.filter('truncate', filter) + +describe('User.vue', () => { + let wrapper + let Wrapper + let propsData + let mocks + let stubs + let getters + let user + + beforeEach(() => { + propsData = {} + + mocks = { + $t: jest.fn() + } + stubs = { + NuxtLink: RouterLinkStub + } + getters = { + 'auth/user': () => { + return {} + }, + 'auth/isModerator': () => false + } + }) + + describe('mount', () => { + const Wrapper = () => { + const store = new Vuex.Store({ + getters + }) + return mount(User, { store, propsData, mocks, stubs, localVue }) + } + + it('renders anonymous user', () => { + const wrapper = Wrapper() + expect(wrapper.text()).not.toMatch('Tilda Swinton') + expect(wrapper.text()).toMatch('Anonymus') + }) + + describe('given an user', () => { + beforeEach(() => { + propsData.user = { + name: 'Tilda Swinton', + slug: 'tilda-swinton' + } + }) + + it('renders user name', () => { + const wrapper = Wrapper() + expect(wrapper.text()).not.toMatch('Anonymous') + expect(wrapper.text()).toMatch('Tilda Swinton') + }) + + describe('user is disabled', () => { + beforeEach(() => { + propsData.user.disabled = true + }) + + it('renders anonymous user', () => { + const wrapper = Wrapper() + expect(wrapper.text()).not.toMatch('Tilda Swinton') + expect(wrapper.text()).toMatch('Anonymus') + }) + + describe('current user is a moderator', () => { + beforeEach(() => { + getters['auth/isModerator'] = () => true + }) + + it('renders user name', () => { + const wrapper = Wrapper() + expect(wrapper.text()).not.toMatch('Anonymous') + expect(wrapper.text()).toMatch('Tilda Swinton') + }) + + it('has "disabled-content" class', () => { + const wrapper = Wrapper() + expect(wrapper.classes()).toContain('disabled-content') + }) + }) + }) + }) + }) +}) diff --git a/components/User.vue b/components/User.vue new file mode 100644 index 000000000..1c78b34cc --- /dev/null +++ b/components/User.vue @@ -0,0 +1,182 @@ + + + + + diff --git a/cypress/integration/common/report.js b/cypress/integration/common/report.js index 6adeb1759..12f1b326f 100644 --- a/cypress/integration/common/report.js +++ b/cypress/integration/common/report.js @@ -64,7 +64,7 @@ When( ) When('I click on the author', () => { - cy.get('a.author') + cy.get('a.user') .first() .click() .wait(200) @@ -112,7 +112,7 @@ When(/^I confirm the reporting dialog .*:$/, message => { cy.contains(message) // wait for element to become visible cy.get('.ds-modal').within(() => { cy.get('button') - .contains('Send Report') + .contains('Report') .click() }) }) diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js index 72a7225ff..eeb3a49d3 100644 --- a/cypress/integration/common/steps.js +++ b/cypress/integration/common/steps.js @@ -155,18 +155,29 @@ When('I press {string}', label => { }) Given('we have the following posts in our database:', table => { - table.hashes().forEach(({ Author, id, title, content }) => { + table.hashes().forEach(({ Author, ...postAttributes }) => { + const userAttributes = { + name: Author, + email: `${Author}@example.org`, + password: '1234' + } + postAttributes.deleted = Boolean(postAttributes.deleted) + const disabled = Boolean(postAttributes.disabled) cy.factory() - .create('User', { - name: Author, - email: `${Author}@example.org`, + .create('User', userAttributes) + .authenticateAs(userAttributes) + .create('Post', postAttributes) + if (disabled) { + const moderatorParams = { + email: 'moderator@example.org', + role: 'moderator', password: '1234' - }) - .authenticateAs({ - email: `${Author}@example.org`, - password: '1234' - }) - .create('Post', { id, title, content }) + } + cy.factory() + .create('User', moderatorParams) + .authenticateAs(moderatorParams) + .mutate('mutation($id: ID!) { disable(id: $id) }', postAttributes) + } }) }) @@ -216,3 +227,20 @@ Then('the post was saved successfully', () => { cy.get('.ds-card-header > .ds-heading').should('contain', lastPost.title) cy.get('.content').should('contain', lastPost.content) }) + +Then(/^I should see only ([0-9]+) posts? on the landing page/, postCount => { + cy.get('.post-card').should('have.length', postCount) +}) + +Then('the first post on the landing page has the title:', title => { + cy.get('.post-card:first').should('contain', title) +}) + +Then( + 'the page {string} returns a 404 error with a message:', + (route, message) => { + // TODO: how can we check HTTP codes with cypress? + cy.visit(route, { failOnStatusCode: false }) + cy.get('.error').should('contain', message) + } +) diff --git a/cypress/integration/moderation/HidePosts.feature b/cypress/integration/moderation/HidePosts.feature new file mode 100644 index 000000000..e886e5f95 --- /dev/null +++ b/cypress/integration/moderation/HidePosts.feature @@ -0,0 +1,26 @@ +Feature: Hide Posts + As the moderator team + we'd like to be able to hide posts from the public + to enforce our network's code of conduct and/or legal regulations + + Background: + Given we have the following posts in our database: + | id | title | deleted | disabled | + | p1 | This post should be visible | | | + | p2 | This post is disabled | | x | + | p3 | This post is deleted | x | | + + Scenario: Disabled posts don't show up on the landing page + Given I am logged in with a "user" role + Then I should see only 1 post on the landing page + And the first post on the landing page has the title: + """ + This post should be visible + """ + + Scenario: Visiting a disabled post's page should return 404 + Given I am logged in with a "user" role + Then the page "/post/this-post-is-disabled" returns a 404 error with a message: + """ + This post could not be found + """ diff --git a/cypress/integration/05.ReportContent.feature b/cypress/integration/moderation/ReportContent.feature similarity index 100% rename from cypress/integration/05.ReportContent.feature rename to cypress/integration/moderation/ReportContent.feature diff --git a/cypress/support/factories.js b/cypress/support/factories.js index 87bcd1853..b9633d434 100644 --- a/cypress/support/factories.js +++ b/cypress/support/factories.js @@ -34,6 +34,14 @@ Cypress.Commands.add( } ) +Cypress.Commands.add( + 'mutate', + { prevSubject: true }, + (factory, mutation, variables) => { + return factory.mutate(mutation, variables) + } +) + Cypress.Commands.add( 'authenticateAs', { prevSubject: true }, diff --git a/graphql/ModerationListQuery.js b/graphql/ModerationListQuery.js index 8ae827d7f..d8105e388 100644 --- a/graphql/ModerationListQuery.js +++ b/graphql/ModerationListQuery.js @@ -9,31 +9,59 @@ export default app => { type createdAt submitter { + disabled + deleted name slug } user { name slug + disabled + deleted + disabledBy { + slug + name + } } comment { contentExcerpt author { name slug + disabled + deleted } post { + disabled + deleted title slug } + disabledBy { + disabled + deleted + slug + name + } } post { title slug + disabled + deleted author { + disabled + deleted name slug } + disabledBy { + disabled + deleted + slug + name + } } } } diff --git a/graphql/UserProfileQuery.js b/graphql/UserProfileQuery.js index 30431602b..683f0e3ac 100644 --- a/graphql/UserProfileQuery.js +++ b/graphql/UserProfileQuery.js @@ -9,6 +9,8 @@ export default app => { name avatar about + disabled + deleted locationName location { name: name${lang} @@ -28,6 +30,8 @@ export default app => { name slug avatar + disabled + deleted followedByCount followedByCurrentUser contributionsCount @@ -46,6 +50,8 @@ export default app => { followedBy(first: 7) { id name + disabled + deleted slug avatar followedByCount @@ -72,6 +78,8 @@ export default app => { deleted image createdAt + disabled + deleted categories { id name @@ -81,6 +89,8 @@ export default app => { id avatar name + disabled + deleted location { name: name${lang} } diff --git a/layouts/default.vue b/layouts/default.vue index c34c02ce4..bdb41f8b2 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -105,10 +105,7 @@
- - - - +
@@ -118,15 +115,16 @@ import { mapGetters, mapActions } from 'vuex' import LocaleSwitch from '~/components/LocaleSwitch' import Dropdown from '~/components/Dropdown' import SearchInput from '~/components/SearchInput.vue' -import ReportModal from '~/components/ReportModal' +import Modal from '~/components/Modal' import seo from '~/components/mixins/seo' export default { components: { Dropdown, - ReportModal, LocaleSwitch, - SearchInput + SearchInput, + Modal, + LocaleSwitch }, mixins: [seo], data() { diff --git a/locales/de.json b/locales/de.json index 7c342fec9..2ec3bac9f 100644 --- a/locales/de.json +++ b/locales/de.json @@ -136,10 +136,14 @@ "reports": { "empty": "Glückwunsch, es gibt nichts zu moderieren.", "name": "Meldungen", - "submitter": "gemeldet von" + "submitter": "gemeldet von", + "disabledBy": "deaktiviert von" } }, "disable": { + "submit": "Deaktivieren", + "cancel": "Abbrechen", + "success": "Erfolgreich deaktiviert", "user": { "title": "Nutzer sperren", "type": "Nutzer", @@ -157,8 +161,9 @@ } }, "report": { - "submit": "Meldung senden", + "submit": "Melden", "cancel": "Abbrechen", + "success": "Vielen Dank für diese Meldung!", "user": { "title": "Nutzer melden", "type": "Nutzer", @@ -181,7 +186,10 @@ }, "comment": { "edit": "Kommentar bearbeiten", - "delete": "Kommentar löschen" + "delete": "Kommentar löschen", + "content": { + "unavailable-placeholder": "...dieser Kommentar ist nicht mehr verfügbar" + } }, "followButton": { "follow": "Folgen", @@ -190,4 +198,4 @@ "shoutButton": { "shouted": "empfohlen" } -} \ No newline at end of file +} diff --git a/locales/en.json b/locales/en.json index f475fcdd2..fe92f901a 100644 --- a/locales/en.json +++ b/locales/en.json @@ -136,10 +136,14 @@ "reports": { "empty": "Congratulations, nothing to moderate.", "name": "Reports", - "submitter": "reported by" + "submitter": "reported by", + "disabledBy": "disabled by" } }, "disable": { + "submit": "Disable", + "cancel": "Cancel", + "success": "Disabled successfully", "user": { "title": "Disable User", "type": "User", @@ -157,8 +161,9 @@ } }, "report": { - "submit": "Send Report", + "submit": "Report", "cancel": "Cancel", + "success": "Thanks for reporting!", "user": { "title": "Report User", "type": "User", @@ -181,7 +186,10 @@ }, "comment": { "edit": "Edit Comment", - "delete": "Delete Comment" + "delete": "Delete Comment", + "content": { + "unavailable-placeholder": "...this comment is not available anymore" + } }, "followButton": { "follow": "Follow", @@ -190,4 +198,4 @@ "shoutButton": { "shouted": "shouted" } -} \ No newline at end of file +} diff --git a/locales/fr.json b/locales/fr.json index 01c2614ac..bb0caaaa3 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -133,7 +133,7 @@ "comment": { "title": "Désactiver le commentaire", "type": "Commentaire", - "message": "Souhaitez-vous vraiment désactiver le commentaire de \"{nom}\" ?" + "message": "Souhaitez-vous vraiment désactiver le commentaire de \"{name}\" ?" } }, "report": { @@ -166,4 +166,4 @@ "edit": "Rédiger un commentaire", "delete": "Supprimer le commentaire" } -} \ No newline at end of file +} diff --git a/nuxt.config.js b/nuxt.config.js index 0d1fbbc07..7f192aa57 100644 --- a/nuxt.config.js +++ b/nuxt.config.js @@ -109,8 +109,7 @@ module.exports = { 'cookie-universal-nuxt', '@nuxtjs/apollo', '@nuxtjs/axios', - '@nuxtjs/style-resources', - 'portal-vue/nuxt' + '@nuxtjs/style-resources' ], /* diff --git a/package.json b/package.json index 0722edd3c..d6580a5bc 100644 --- a/package.json +++ b/package.json @@ -54,10 +54,9 @@ "linkify-it": "~2.1.0", "nuxt": "~2.4.5", "nuxt-env": "~0.1.0", - "portal-vue": "~1.5.1", - "string-hash": "~1.1.3", - "tiptap": "~1.14.0", - "tiptap-extensions": "~1.14.0", + "string-hash": "^1.1.3", + "tiptap": "^1.14.0", + "tiptap-extensions": "^1.14.0", "v-tooltip": "~2.0.0-rc.33", "vue-count-to": "~1.0.13", "vue-izitoast": "1.1.2", diff --git a/pages/index.vue b/pages/index.vue index a9c78f4b8..e28d6b2c3 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -97,6 +97,8 @@ export default { title contentExcerpt createdAt + disabled + deleted slug image author { @@ -104,6 +106,8 @@ export default { avatar slug name + disabled + deleted contributionsCount shoutedCount commentsCount diff --git a/pages/moderation/index.vue b/pages/moderation/index.vue index 209f8dcba..cd41dc17c 100644 --- a/pages/moderation/index.vue +++ b/pages/moderation/index.vue @@ -73,6 +73,29 @@ {{ scope.row.submitter.name }} + - - - - + + + + + + + + +
- - - - - -
- - - - - -   - - + + + + +   + + - - - - - -

- - - + + + + +

+ + + + {{ post.commentsCount }} +   Comments + +

+ +
+ +
+ +
+ + diff --git a/pages/post/edit/_id.vue b/pages/post/edit/_id.vue index 156bcfd23..5f6a3b271 100644 --- a/pages/post/edit/_id.vue +++ b/pages/post/edit/_id.vue @@ -48,10 +48,14 @@ export default { title content createdAt + disabled + deleted slug image author { id + disabled + deleted } tags { name diff --git a/pages/profile/_slug.vue b/pages/profile/_slug.vue index 614187004..76011f456 100644 --- a/pages/profile/_slug.vue +++ b/pages/profile/_slug.vue @@ -10,7 +10,10 @@ gutter="base" > - + @@ -134,8 +136,8 @@ > - @@ -179,8 +181,8 @@ > - @@ -273,7 +275,6 @@ > @@ -299,7 +300,7 @@