Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

Create the Content Report flow #720

Merged
merged 25 commits into from
Feb 9, 2022
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6f57913
Move `VCheckbox` into its own directory
dhruvkb Feb 4, 2022
ff94866
Define `VRadio` as a companion to `VCheckbox`
dhruvkb Feb 4, 2022
2d9c4ff
Add i18n for content reports
dhruvkb Feb 4, 2022
8438bd2
Allow whitespace in popover to wrap as needed
dhruvkb Feb 4, 2022
3300e76
Create the content report flow components
dhruvkb Feb 4, 2022
b3b0a36
Replace the dummy text with a working report feature
dhruvkb Feb 4, 2022
cb7a19d
Register `VRadio` component
dhruvkb Feb 4, 2022
07a86d8
Separate strings for button text
dhruvkb Feb 4, 2022
def0345
Update specs to match the new flow
dhruvkb Feb 4, 2022
6064bf5
Consistently use `~` for referring to the `src/` dir
dhruvkb Feb 4, 2022
aac9015
Make the popover render above everything else
dhruvkb Feb 4, 2022
40fbc48
Remove the character counter
dhruvkb Feb 4, 2022
551c696
Do away with the popover closing hack
dhruvkb Feb 4, 2022
05c1593
Close popup when the 'Cancel' option is clicked
dhruvkb Feb 4, 2022
6e11775
Add 'noreferrer' to the `rel` attr
dhruvkb Feb 4, 2022
d6be492
Use a clearer prop name for the foreign landing URL
dhruvkb Feb 4, 2022
7934d4b
Implement updated flow for fixed height
dhruvkb Feb 7, 2022
0e521a4
Make the submit button focusable when disabled
dhruvkb Feb 7, 2022
2087696
Fix the issue with double scrollbars
dhruvkb Feb 7, 2022
0c7f2ce
Move the close button above the form in terms of the DOM order
dhruvkb Feb 7, 2022
4d8bd5e
Add min and max length to the form
dhruvkb Feb 8, 2022
ccc0a63
Merge branch 'main' into report_flow
dhruvkb Feb 8, 2022
db88298
Change import paths from '@' to '~'
dhruvkb Feb 8, 2022
51ca14f
Make the checkbox and radio SVGs ARIA-hidden
dhruvkb Feb 8, 2022
7fff77b
Update description field as per selected reason
dhruvkb Feb 9, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/assets/icons/radiomark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import VCheckbox from '~/components/VCheckbox'
import VLicense from '~/components/License/VLicense'
import VCheckbox from '~/components/VCheckbox/VCheckbox.vue'
import VLicense from '~/components/License/VLicense.vue'

export default {
title: 'Components/VCheckbox',
Expand Down
40 changes: 40 additions & 0 deletions src/components/VContentReport/VContentReportButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<template>
<VButton
variant="plain"
class="report-button font-semibold text-dark-charcoal"
>
<span class="md:hidden">{{
$t('media-details.content-report.short')
}}</span>
<span class="hidden md:inline">{{
$t('media-details.content-report.long')
}}</span>
<VIcon :icon-path="icons.flag" class="ms-2" />
</VButton>
</template>

<script>
import { defineComponent } from '@nuxtjs/composition-api'

import VIcon from '~/components/VIcon/VIcon.vue'

import flagIcon from '~/assets/icons/flag.svg'

export default defineComponent({
name: 'VContentReportButton',
components: { VIcon },
setup() {
return {
icons: {
flag: flagIcon,
},
}
},
})
</script>

<style scoped>
.report-button {
@apply px-0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the goal for removing this? It makes the focus ring really "tight" around the text and the icon 🙁

Copy link
Member Author

@dhruvkb dhruvkb Feb 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the horizontal padding the report button appears noticeably out-of-alignment with the right side of the page.

Screenshot 2022-02-04 at 19-50-49 Cat - Openverse

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see.

@panchovm how would you like these cases handled? Perhaps we shouldn't use a focus outline in these cases? We could translate the button horizontally to align it with the end rule of the page but that could cause unwanted horizontal scrolling in narrower screens if we're not careful.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would follow the Gutenberg approach, prioritizing the visual alignment for elements having no border like the Edit action in the gif below.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's how it currently is. The focus outline appears a little closer to the text, but brings the button closer to the right edge.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, if it's fine for the focus-outline to hug the text like that then disregard this comment 🙂

}
</style>
304 changes: 155 additions & 149 deletions src/components/VContentReport/VContentReportForm.vue
Original file line number Diff line number Diff line change
@@ -1,180 +1,186 @@
<template>
<div id="content-report-form" class="p-4">
<button
:aria-label="$t('photo-details.aria.close-form')"
class="button close-button is-text tiny float-right block bg-white"
type="button"
@click="closeForm"
>
<VIcon :icon-path="closeIcon" />
</button>
<VDmcaNotice
v-if="showDmcaForm"
:image-url="image.foreign_landing_url"
:provider="providerName"
:dmca-form-url="dmcaFormUrl"
@back-click="onBackClick"
/>
<VDoneMessage
v-else-if="isDone"
:image-url="image.foreign_landing_url"
:provider="providerName"
/>
<VReportError v-else-if="reportFailed" @back-click="backToReportStart" />

<VOtherIssueForm
v-else-if="showOtherForm"
@back-click="onBackClick"
@send-report="sendContentReport"
/>
<form v-else>
<h5 class="b-header mb-4">
{{ $t('photo-details.content-report.title') }}
</h5>
<fieldset class="mb-4 flex flex-col">
<legend class="mb-4">
{{ $t('photo-details.content-report.issue') }}
</legend>
<label
v-for="reason in reasons"
:key="reason"
:for="reason"
class="ms-2 mb-2"
>
<input
:id="reason"
v-model="reasonSelected"
type="radio"
name="type"
:value="reason"
/>
{{ $t(`photo-details.content-report.reasons.${reason}`) }}
</label>
</fieldset>

<p class="caption font-semibold text-gray mb-4">
{{ $t('photo-details.content-report.caption') }}
<div id="content-report-form" class="w-80 p-6">
<div v-if="status === statuses.SENT">
<p class="font-semibold text-2xl mb-4">
{{ $t('media-details.content-report.success.title') }}
</p>

<VButton
:disabled="!reasonSelected"
variant="secondary"
class="float-end bg-trans-blue"
@click="onIssueSelected"
<i18n
path="media-details.content-report.success.note"
class="text-sm"
tag="p"
>
{{ $t('photo-details.content-report.next') }}
</VButton>
</form>
<template #source>
<a
:href="media.url"
class="text-pink hover:underline"
target="_blank"
rel="noopener"
>{{ providerName }}</a
>
</template>
</i18n>
</div>

<div v-else-if="status === statuses.FAILED">
<p class="font-semibold text-2xl mb-4">
{{ $t('media-details.content-report.failure.title') }}
</p>
<p class="text-sm">
{{ $t('media-details.content-report.failure.note') }}
</p>
</div>

<!-- Main form -->
<div v-else>
<div class="font-semibold text-2xl mb-4">
{{ $t('media-details.content-report.long') }}
</div>

<p class="text-sm mb-4">
{{ $t('media-details.content-report.form.disclaimer') }}
</p>

<form class="text-sm">
<fieldset class="mb-4 flex flex-col">
<legend class="font-semibold mb-4">
{{ $t('media-details.content-report.form.question') }}
</legend>
<template v-for="reason in reasons">
<VRadio
:id="reason"
:key="reason"
v-model="selectedReason"
class="mb-4"
name="reason"
:value="reason"
>
<div class="flex flex-col gap-2 w-full">
<div>
{{ $t(`media-details.content-report.form.${reason}.option`) }}
</div>
<VDmcaNotice
v-if="reason === reasons.DMCA"
v-show="reason === selectedReason"
:provider="providerName"
:media-url="media.foreign_landing_url"
/>
<VOtherIssueForm
v-if="reason === reasons.OTHER"
v-show="reason === selectedReason"
key="other"
v-model="description"
/>
</div>
</VRadio>
</template>
</fieldset>

<div class="flex flex-row items-center justify-end gap-4">
<VButton variant="tertiary" @click="handleCancel">
{{ $t('media-details.content-report.form.cancel') }}
</VButton>

<VButton
v-if="selectedReason === reasons.DMCA"
key="dmca"
dhruvkb marked this conversation as resolved.
Show resolved Hide resolved
as="a"
variant="secondary"
:href="DMCA_FORM_URL"
target="_blank"
rel="noopener"
dhruvkb marked this conversation as resolved.
Show resolved Hide resolved
>
{{ $t('media-details.content-report.form.dmca.open') }}
<VIcon :size="4" class="ms-1" :icon-path="icons.externalLink" />
</VButton>
<VButton
v-else
key="non-dmca"
:disabled="isSubmitDisabled"
variant="secondary"
@click="handleSubmit"
>
{{ $t('media-details.content-report.form.submit') }}
</VButton>
</div>
</form>
</div>
</div>
</template>

<script>
import { computed, defineComponent, ref } from '@nuxtjs/composition-api'
import VDmcaNotice from './VDmcaNotice'
import VOtherIssueForm from './VOtherIssueForm'
import VDoneMessage from './VDoneMessage'
import VReportError from './VReportError'
import ReportService from '~/data/report-service'

import closeIcon from '~/assets/icons/close.svg'
import VIcon from '~/components/VIcon/VIcon.vue'
import VButton from '~/components/VButton.vue'
import VRadio from '~/components/VRadio/VRadio.vue'
import VDmcaNotice from '~/components/VContentReport/VDmcaNotice.vue'
import VOtherIssueForm from '~/components/VContentReport/VOtherIssueForm.vue'

import ReportService from '~/data/report-service'

import { reasons, statuses, DMCA_FORM_URL } from '@/constants/content-report'
dhruvkb marked this conversation as resolved.
Show resolved Hide resolved

const dmcaFormUrl =
'https://docs.google.com/forms/d/e/1FAIpQLSd0I8GsEbGQLdaX4K_F6V2NbHZqN137WMZgnptUpzwd-kbDKA/viewform'
const reasons = {
DMCA: 'dmca',
MATURE: 'mature',
OTHER: 'other',
}
const statuses = {
SENT: 'sent',
FAILED: 'failed',
ADDING_DETAILS: 'adding_details',
OPEN: 'open',
CLOSED: 'closed',
}
import externalLinkIcon from '~/assets/icons/external-link.svg'

export default defineComponent({
name: 'VContentReportForm',
components: {
VDoneMessage,
VButton,
VRadio,
VDmcaNotice,
VReportError,
VOtherIssueForm,
VButton,
VIcon,
},
props: ['image', 'providerName', 'reportService'],
setup(props, { emit }) {
const reportStatus = ref(statuses.OPEN)
props: ['media', 'image', 'providerName', 'reportService'],
setup(props) {
const service = props.reportService || ReportService

/** @type {import('@nuxtjs/composition-api').Ref<string|null>} */
const reasonSelected = ref(null)
const status = ref(statuses.WIP)
const selectedReason = ref(null)
const description = ref('')

const service = props.reportService || ReportService
const onIssueSelected = () => {
if (
reasonSelected.value &&
![reasons.OTHER, reasons.DMCA].includes(reasonSelected.value)
) {
sendContentReport()
} else {
reportStatus.value = statuses.ADDING_DETAILS
}
}
const onBackClick = () => {
reportStatus.value = statuses.OPEN
/* Buttons */
const handleCancel = () => {
dhruvkb marked this conversation as resolved.
Show resolved Hide resolved
selectedReason.value = null
description.value = ''
}
const backToReportStart = () => {
reasonSelected.value = null
reportStatus.value = statuses.OPEN
}
const sendContentReport = async ({ description = '' } = {}) => {

const isSubmitDisabled = computed(
() =>
selectedReason.value === null ||
(selectedReason.value === reasons.OTHER &&
description.value.length < 20)
)
const handleSubmit = async () => {
if (selectedReason.value === reasons.DMCA) return
// Submit report
try {
await service.sendReport({
identifier: props.image.id,
reason: reasonSelected.value,
description,
identifier: props.media.identifier,
reason: selectedReason.value,
description: description.value,
})
reportStatus.value = statuses.SENT
status.value = statuses.SENT
} catch (error) {
reportStatus.value = statuses.FAILED
status.value = statuses.FAILED
}
}
const closeForm = () => {
reportStatus.value = statuses.CLOSED
emit('close-form')
}
const reportFailed = computed(() => reportStatus.value === statuses.FAILED)
const isDone = computed(
() =>
reportStatus.value === statuses.SENT &&
!(reasonSelected.value === reasons.DMCA)
)
const showOtherForm = computed(
() =>
reportStatus.value === statuses.ADDING_DETAILS &&
reasonSelected.value === reasons.OTHER
)
const showDmcaForm = computed(
() =>
reportStatus.value === statuses.ADDING_DETAILS &&
reasonSelected.value === reasons.DMCA
)

return {
reasonSelected,
closeIcon,
dmcaFormUrl,
isDone,
reportFailed,
closeForm,
onIssueSelected,
onBackClick,
sendContentReport,
backToReportStart,
showOtherForm,
showDmcaForm,
reasons: Object.values(reasons),
icons: {
externalLink: externalLinkIcon,
},
reasons,
statuses,
DMCA_FORM_URL,

selectedReason,
status,
description,

handleCancel,

isSubmitDisabled,
handleSubmit,
}
},
})
Expand Down
Loading