Move components to components/features

- we are reserving _new directory for components that have no dependency
on the Nitro-Styleguide
- update pat
This commit is contained in:
mattwr18 2019-12-04 19:37:46 +01:00
parent 26b4d4ab93
commit 235702867d
7 changed files with 1 additions and 831 deletions

View File

@ -1,68 +0,0 @@
<template>
<ds-table
class="nested-table"
v-if="filed && filed.length"
:data="filed"
:fields="reportFields"
condensed
>
<template #submitter="scope">
<hc-user :user="scope.row.submitter" :showAvatar="false" :trunc="30" />
</template>
<template #reportedOn="scope">
<ds-text size="small">
<hc-relative-date-time :date-time="scope.row.createdAt" />
</ds-text>
</template>
<template #reasonCategory="scope">
{{ $t('report.reason.category.options.' + scope.row.reasonCategory) }}
</template>
<template #reasonDescription="scope">
{{ scope.row.reasonDescription.length ? scope.row.reasonDescription : '—' }}
</template>
</ds-table>
</template>
<script>
import HcUser from '~/components/User/User'
import HcRelativeDateTime from '~/components/RelativeDateTime'
export default {
components: {
HcUser,
HcRelativeDateTime,
},
props: {
filed: { type: Array, default: () => [] },
},
computed: {
reportFields() {
return {
submitter: {
label: this.$t('moderation.reports.submitter'),
width: '15%',
},
reportedOn: {
label: this.$t('moderation.reports.reportedOn'),
width: '20%',
},
reasonCategory: {
label: this.$t('moderation.reports.reasonCategory'),
width: '30%',
},
reasonDescription: {
label: this.$t('moderation.reports.reasonDescription'),
width: '35%',
},
}
},
},
}
</script>
<style lang="scss">
.nested-table {
padding: $space-small;
border-top: $border-size-base solid $color-neutral-60;
border-bottom: $border-size-base solid $color-neutral-60;
}
</style>

View File

@ -1,142 +0,0 @@
<template>
<ds-card>
<div class="reports-header">
<h3 class="title">{{ $t('moderation.reports.name') }}</h3>
<client-only>
<dropdown-filter @filter="filter" :filterOptions="filterOptions" :selected="selected" />
</client-only>
</div>
<reports-table :reports="reports" @confirm="openModal" />
</ds-card>
</template>
<script>
import { mapMutations } from 'vuex'
import DropdownFilter from '~/components/DropdownFilter/DropdownFilter'
import ReportsTable from '~/components/_new/features/ReportsTable/ReportsTable'
import { reportsListQuery, reviewMutation } from '~/graphql/Moderation.js'
export default {
components: {
DropdownFilter,
ReportsTable,
},
data() {
return {
reports: [],
allReports: [],
unreviewedReports: [],
reviewedReports: [],
closedReports: [],
selected: this.$t('moderation.reports.filterLabel.all'),
}
},
computed: {
filterOptions() {
return [
{ label: this.$t('moderation.reports.filterLabel.all'), value: this.allReports },
{
label: this.$t('moderation.reports.filterLabel.unreviewed'),
value: this.unreviewedReports,
},
{ label: this.$t('moderation.reports.filterLabel.reviewed'), value: this.reviewedReports },
{ label: this.$t('moderation.reports.filterLabel.closed'), value: this.closedReports },
]
},
modalData() {
return function(report) {
const identStart =
'moderation.reports.decideModal.' +
report.resource.__typename +
'.' +
(report.resource.disabled ? 'disable' : 'enable')
return {
name: 'confirm',
data: {
type: report.resource.__typename,
resource: report.resource,
modalData: {
titleIdent: identStart + '.title',
messageIdent: identStart + '.message',
messageParams: {
name:
report.resource.name ||
this.$filters.truncate(report.resource.title, 30) ||
this.$filters.truncate(
this.$filters.removeHtml(report.resource.contentExcerpt),
30,
),
},
buttons: {
confirm: {
danger: true,
icon: report.resource.disabled ? 'eye-slash' : 'eye',
textIdent: 'moderation.reports.decideModal.submit',
callback: () => {
this.confirmCallback(report.resource)
},
},
cancel: {
icon: 'close',
textIdent: 'moderation.reports.decideModal.cancel',
callback: () => {},
},
},
},
},
}
}
},
},
methods: {
...mapMutations({
commitModalData: 'modal/SET_OPEN',
}),
filter(option) {
this.reports = option.value
this.selected = option.label
},
async confirmCallback(resource) {
const { disabled: disable, id: resourceId } = resource
this.$apollo
.mutate({
mutation: reviewMutation(),
variables: { disable, resourceId, closed: true },
})
.then(() => {
this.$toast.success(this.$t('moderation.reports.DecisionSuccess'))
this.$apollo.queries.reportsList.refetch()
})
.catch(error => this.$toast.error(error.message))
},
openModal(report) {
this.commitModalData(this.modalData(report))
},
},
apollo: {
reportsList: {
query: reportsListQuery(),
update({ reports }) {
this.reports = reports
this.allReports = reports
this.unreviewedReports = reports.filter(report => !report.reviewed)
this.reviewedReports = reports.filter(report => report.reviewed)
this.closedReports = reports.filter(report => report.closed)
},
fetchPolicy: 'cache-and-network',
},
},
}
</script>
<style lang="scss">
.reports-header {
display: flex;
justify-content: space-between;
margin: $space-small 0;
> .title {
margin: 0;
font-size: $font-size-large;
}
}
</style>

View File

@ -1,185 +0,0 @@
<template>
<tbody class="report-row" :data-test="dataTest">
<tr>
<!-- Icon Column -->
<td class="ds-table-col">
<base-icon :name="iconName" :title="iconLabel" />
</td>
<!-- Number of Filed Reports Column -->
<td class="ds-table-col">
<span class="user-count">
{{ $t('moderation.reports.numberOfUsers', { count: report.filed.length }) }}
</span>
<ds-button size="small" @click="showFiledReports = !showFiledReports">
{{ $t('moderation.reports.moreDetails') }}
</ds-button>
</td>
<!-- Content Column -->
<td class="ds-table-col" data-test="report-content">
<client-only v-if="isUser">
<hc-user :user="report.resource" :showAvatar="false" :trunc="30" />
</client-only>
<nuxt-link v-else class="title" :to="linkTarget">
{{ linkText | truncate(50) }}
</nuxt-link>
</td>
<!-- Author Column -->
<td class="ds-table-col" data-test="report-author">
<client-only v-if="!isUser">
<hc-user :user="report.resource.author" :showAvatar="false" :trunc="30" />
</client-only>
<span v-else></span>
</td>
<!-- Status Column -->
<td class="ds-table-col" data-test="report-reviewer">
<span class="status-line">
<base-icon :name="statusIconName" :class="isDisabled ? '--disabled' : '--enabled'" />
{{ statusText }}
</span>
<client-only v-if="report.reviewed">
<hc-user
:user="moderatorOfLatestReview"
:showAvatar="false"
:trunc="30"
:date-time="report.updatedAt"
/>
</client-only>
</td>
<!-- Decision Column -->
<td class="ds-table-col">
<b v-if="report.closed" data-test="report-closed">
{{ $t('moderation.reports.decided') }}
</b>
<ds-button
v-else
danger
size="small"
:icon="statusIconName"
@click="$emit('confirm-report')"
>
{{ $t('moderation.reports.decideButton') }}
</ds-button>
</td>
</tr>
<tr class="row">
<td colspan="100%">
<filed-table :filed="report.filed" v-if="showFiledReports" />
</td>
</tr>
</tbody>
</template>
<script>
import FiledTable from '~/components/_new/features/FiledTable/FiledTable'
import HcUser from '~/components/User/User'
export default {
components: {
FiledTable,
HcUser,
},
props: {
report: {
type: Object,
required: true,
},
},
data() {
return {
showFiledReports: false,
}
},
computed: {
dataTest() {
return `report-${this.report.resource.__typename}`.toLowerCase()
},
isPost() {
return this.report.resource.__typename === 'Post'
},
isComment() {
return this.report.resource.__typename === 'Comment'
},
isUser() {
return this.report.resource.__typename === 'User'
},
isDisabled() {
return this.report.resource.disabled
},
iconName() {
if (this.isPost) return 'bookmark'
else if (this.isComment) return 'comments'
else if (this.isUser) return 'user'
else return null
},
iconLabel() {
if (this.isPost) return this.$t('report.contribution.type')
else if (this.isComment) return this.$t('report.comment.type')
else if (this.isUser) return this.$t('report.user.type')
else return null
},
linkTarget() {
const { id, slug } = this.isComment ? this.report.resource.post : this.report.resource
return {
name: 'post-id-slug',
params: { id, slug },
hash: this.isComment ? `#commentId-${this.report.resource.id}` : '',
}
},
linkText() {
return (
this.report.resource.title || this.$filters.removeHtml(this.report.resource.contentExcerpt)
)
},
statusIconName() {
return this.isDisabled ? 'eye-slash' : 'eye'
},
statusText() {
if (!this.report.reviewed) return this.$t('moderation.reports.enabled')
else if (this.isDisabled) return this.$t('moderation.reports.disabledBy')
else return this.$t('moderation.reports.enabledBy')
},
moderatorOfLatestReview() {
return this.report.reviewed[0].moderator
},
},
}
</script>
<style lang="scss">
.report-row {
&:nth-child(2n + 1) {
background-color: $color-neutral-95;
}
.title {
font-weight: $font-weight-bold;
}
.status-line {
display: inline-flex;
> .base-icon {
margin-right: $space-xx-small;
}
}
.user-count {
display: block;
margin-bottom: $space-xx-small;
}
.--disabled {
color: $color-danger;
}
.--enabled {
color: $color-success;
}
}
</style>

View File

@ -1,216 +0,0 @@
import { config, mount, RouterLinkStub } from '@vue/test-utils'
import Vuex from 'vuex'
import ReportsTable from './ReportsTable.vue'
import { reports } from './ReportsTable.story.js'
import BaseIcon from '~/components/_new/generic/BaseIcon/BaseIcon'
const localVue = global.localVue
config.stubs['client-only'] = '<span><slot /></span>'
describe('ReportsTable', () => {
let propsData, mocks, stubs, getters, wrapper, reportsTable
beforeEach(() => {
propsData = {}
mocks = {
$t: jest.fn(string => string),
}
stubs = {
NuxtLink: RouterLinkStub,
}
getters = {
'auth/user': () => {
return { slug: 'awesome-user' }
},
'auth/isModerator': () => true,
}
})
describe('mount', () => {
const Wrapper = () => {
const store = new Vuex.Store({
getters,
})
return mount(ReportsTable, { propsData, mocks, stubs, localVue, store })
}
describe('given no reports', () => {
beforeEach(() => {
propsData = { ...propsData, reports: [] }
wrapper = Wrapper()
})
it('shows a placeholder', () => {
expect(wrapper.text()).toContain('moderation.reports.empty')
})
})
describe('given reports', () => {
beforeEach(() => {
propsData = { ...propsData, reports }
wrapper = Wrapper()
reportsTable = wrapper.find('.ds-table')
})
it('renders a table', () => {
expect(reportsTable.exists()).toBe(true)
})
describe('Comment', () => {
let commentRow
beforeEach(() => {
commentRow = wrapper.find('[data-test="report-comment"]')
})
it('renders a comments icon', () => {
const commentsIcon = commentRow.find(BaseIcon).props().name
expect(commentsIcon).toEqual('comments')
})
it('renders a link to the post, with the comment contentExcerpt', () => {
const postLink = commentRow.find('[data-test="report-content"] a')
expect(postLink.text()).toEqual('@peter-lustig Lorem ipsum dolor sit amet, …')
})
it('renders the author', () => {
const username = commentRow.find('[data-test="report-author"] b')
expect(username.text()).toEqual('Louie')
})
describe('given it has been reviewed', () => {
it('renders the enabled icon', () => {
expect(
commentRow
.find('.status-line')
.find(BaseIcon)
.props().name,
).toEqual('eye')
})
it('renders its current status', () => {
expect(commentRow.find('.status-line').text()).toEqual('moderation.reports.enabledBy')
})
it('renders the moderator who reviewed the resource', () => {
const username = commentRow.find('[data-test="report-reviewer"]')
expect(username.text()).toContain('@moderator')
})
})
describe('give report has not been closed', () => {
let confirmButton
beforeEach(() => {
confirmButton = commentRow.find('[data-test="confirm"]')
})
it('renders a confirm button', () => {
expect(confirmButton.exists()).toBe(true)
})
it('emits confirm event', () => {
confirmButton.trigger('click')
expect(wrapper.emitted().confirm[0][0]).toEqual(reports[0])
})
})
})
describe('Post', () => {
let postRow
beforeEach(() => {
postRow = wrapper.find('[data-test="report-post"]')
})
it('renders a bookmark icon', () => {
const postIcon = postRow.find(BaseIcon).props().name
expect(postIcon).toEqual('bookmark')
})
it('renders a link to the post', () => {
const postLink = postRow.find('[data-test="report-content"] a')
expect(postLink.text()).toEqual("I'm a bigoted post!")
})
it('renders the author', () => {
const username = postRow.find('[data-test="report-author"]')
expect(username.text()).toContain('Dagobert')
})
describe('given it has not been reviewed', () => {
it('renders the enabled icon', () => {
expect(
postRow
.find('.status-line')
.find(BaseIcon)
.props().name,
).toEqual('eye')
})
it('renders its current status', () => {
expect(postRow.find('.status-line').text()).toEqual('moderation.reports.enabled')
})
})
describe('give report has not been closed', () => {
let confirmButton
beforeEach(() => {
confirmButton = postRow.find('[data-test="confirm"]')
})
it('renders a confirm button', () => {
expect(confirmButton.exists()).toBe(true)
})
it('emits confirm event', () => {
confirmButton.trigger('click')
expect(wrapper.emitted().confirm[0][0]).toEqual(reports[1])
})
})
})
describe('User', () => {
let userRow
beforeEach(() => {
userRow = wrapper.find('[data-test="report-user"]')
})
it('renders a bookmark icon', () => {
const userIcon = userRow.find(BaseIcon).props().name
expect(userIcon).toEqual('user')
})
it('renders a link to the user profile', () => {
const userLink = userRow.find('[data-test="report-content"] a')
expect(userLink.text()).toContain('Abusive user')
})
describe('given it has been reviewed', () => {
it('renders the disabled icon', () => {
expect(
userRow
.find('.status-line')
.find(BaseIcon)
.props().name,
).toEqual('eye-slash')
})
it('renders its current status', () => {
expect(userRow.find('.status-line').text()).toEqual('moderation.reports.disabledBy')
})
it('renders the moderator who reviewed the resource', () => {
const username = userRow.find('[data-test="report-reviewer"]')
expect(username.text()).toContain('Peter Lustig')
})
})
describe('give report has been closed', () => {
it('renders Decided text', () => {
expect(userRow.find('[data-test="report-closed"]').text()).toEqual(
'moderation.reports.decided',
)
})
})
})
})
})
})

View File

@ -1,169 +0,0 @@
import { storiesOf } from '@storybook/vue'
import { withA11y } from '@storybook/addon-a11y'
import { action } from '@storybook/addon-actions'
import ReportsTable from '~/components/_new/features/ReportsTable/ReportsTable'
import helpers from '~/storybook/helpers'
import { post } from '~/components/PostCard/PostCard.story.js'
import { user } from '~/components/User/User.story.js'
helpers.init()
export const reports = [
{
__typename: 'Report',
closed: false,
createdAt: '2019-10-29T15:36:02.106Z',
updatedAt: '2019-12-02T15:56:35.651Z',
disable: false,
filed: [
{
__typename: 'FILED',
createdAt: '2019-12-02T15:56:35.676Z',
reasonCategory: 'pornographic_content_links',
reasonDescription: 'This comment is porno!!!',
submitter: {
...user,
},
},
],
resource: {
__typename: 'Comment',
id: 'b6b38937-3efc-4d5e-b12c-549e4d6551a5',
createdAt: '2019-10-29T15:38:25.184Z',
updatedAt: '2019-10-30T15:38:25.184Z',
disabled: false,
deleted: false,
content:
'<p><a class="mention" href="/profile/u1" data-mention-id="u1" target="_blank">@peter-lustig</a> </p><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Turpis egestas pretium aenean pharetra magna ac placerat. Tempor id eu nisl nunc mi ipsum faucibus vitae. Nibh praesent tristique magna sit amet purus gravida quis blandit. Magna eget est lorem ipsum dolor. In fermentum posuere urna nec. Eleifend donec pretium vulputate sapien nec sagittis aliquam. Augue interdum velit euismod in pellentesque. Id diam maecenas ultricies mi eget mauris pharetra. Donec pretium vulputate sapien nec. Dolor morbi non arcu risus quis varius quam quisque. Blandit turpis cursus in hac habitasse. Est ultricies integer quis auctor elit sed vulputate mi sit. Nunc consequat interdum varius sit amet mattis vulputate enim. Semper feugiat nibh sed pulvinar. Eget felis eget nunc lobortis mattis aliquam. Ultrices vitae auctor eu augue. Tellus molestie nunc non blandit massa enim nec dui. Pharetra massa massa ultricies mi quis hendrerit dolor. Nisl suscipit adipiscing bibendum est ultricies integer.</p>',
contentExcerpt:
'<p><a href="/profile/u1" target="_blank">@peter-lustig</a> </p><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Turpis egestas pretium aenean pharetra …</p>',
post,
author: user,
},
reviewed: [
{
updatedAt: '2019-10-30T15:38:25.184Z',
moderator: {
__typename: 'User',
...user,
name: 'Moderator',
id: 'moderator',
slug: 'moderator',
},
},
{
updatedAt: '2019-10-29T15:38:25.184Z',
moderator: {
__typename: 'User',
...user,
name: 'Peter Lustig',
id: 'u3',
slug: 'peter-lustig',
},
},
],
},
{
__typename: 'Report',
closed: false,
createdAt: '2019-10-31T15:36:02.106Z',
updatedAt: '2019-12-03T15:56:35.651Z',
disable: true,
filed: [
{
__typename: 'FILED',
createdAt: '2019-10-31T15:36:02.106Z',
reasonCategory: 'discrimination_etc',
reasonDescription: 'This post is bigoted',
submitter: {
...user,
},
},
],
resource: {
__typename: 'Post',
author: {
...user,
id: 'u7',
name: 'Dagobert',
slug: 'dagobert',
},
deleted: false,
disabled: false,
id: 'p2',
slug: 'bigoted-post',
title: "I'm a bigoted post!",
},
reviewed: null,
},
{
__typename: 'Report',
closed: true,
createdAt: '2019-10-30T15:36:02.106Z',
updatedAt: '2019-12-01T15:56:35.651Z',
disable: true,
filed: [
{
__typename: 'FILED',
createdAt: '2019-10-30T15:36:02.106Z',
reasonCategory: 'discrimination_etc',
reasonDescription: 'this user is attacking me for who I am!',
submitter: {
...user,
},
},
],
resource: {
__typename: 'User',
commentedCount: 0,
contributionsCount: 0,
deleted: false,
disabled: true,
followedByCount: 0,
id: 'u5',
name: 'Abusive user',
slug: 'abusive-user',
},
reviewed: [
{
updatedAt: '2019-12-01T15:56:35.651Z',
moderator: {
__typename: 'User',
...user,
name: 'Peter Lustig',
id: 'u3',
slug: 'peter-lustig',
},
},
{
updatedAt: '2019-11-30T15:56:35.651Z',
moderator: {
__typename: 'User',
...user,
name: 'Moderator',
id: 'moderator',
slug: 'moderator',
},
},
],
},
]
storiesOf('ReportsTable', module)
.addDecorator(withA11y)
.addDecorator(helpers.layout)
.add('with reports', () => ({
components: { ReportsTable },
store: helpers.store,
data: () => ({
reports,
}),
methods: {
confirm: action('confirm'),
},
template: `<reports-table :reports="reports" @confirm="confirm" />`,
}))
.add('without reports', () => ({
components: { ReportsTable },
store: helpers.store,
template: `<reports-table />`,
}))

View File

@ -1,50 +0,0 @@
<template>
<table
v-if="reports && reports.length"
class="ds-table ds-table-condensed ds-table-bordered reports-table"
cellspacing="0"
cellpadding="0"
>
<colgroup>
<col width="4%" />
<col width="14%" />
<col width="36%" />
<col width="14%" />
<col width="20%" />
<col width="12%" />
</colgroup>
<thead class="ds-table-col ds-table-head-col">
<tr valign="top">
<th class="ds-table-head-col"></th>
<th class="ds-table-head-col">{{ $t('moderation.reports.submitter') }}</th>
<th class="ds-table-head-col">{{ $t('moderation.reports.content') }}</th>
<th class="ds-table-head-col">{{ $t('moderation.reports.author') }}</th>
<th class="ds-table-head-col">{{ $t('moderation.reports.status') }}</th>
<th class="ds-table-head-col">{{ $t('moderation.reports.decision') }}</th>
</tr>
</thead>
<template v-for="report in reports">
<report-row
:key="report.resource.id"
:report="report"
@confirm-report="$emit('confirm', report)"
/>
</template>
</table>
<hc-empty v-else icon="alert" :message="$t('moderation.reports.empty')" />
</template>
<script>
import HcEmpty from '~/components/Empty/Empty'
import ReportRow from '~/components/_new/features/ReportRow/ReportRow'
export default {
components: {
HcEmpty,
ReportRow,
},
props: {
reports: { type: Array, default: () => [] },
},
}
</script>

View File

@ -3,7 +3,7 @@
</template>
<script>
import ReportList from '~/components/_new/features/ReportList/ReportList'
import ReportList from '~/components/features/ReportList/ReportList'
export default {
components: {