Switching to a named filters/single input on video-abuse
This commit is contained in:
parent
c2a89b70ce
commit
0d3a2982a9
|
@ -12,6 +12,7 @@
|
||||||
|
|
||||||
input {
|
input {
|
||||||
@include peertube-input-text(250px);
|
@include peertube-input-text(250px);
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,10 +7,32 @@
|
||||||
<ng-template pTemplate="caption">
|
<ng-template pTemplate="caption">
|
||||||
<div class="caption">
|
<div class="caption">
|
||||||
<div class="ml-auto">
|
<div class="ml-auto">
|
||||||
<input
|
<div class="input-group">
|
||||||
type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
|
<div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body">
|
||||||
(keyup)="onSearch($event)"
|
<div class="input-group-text" ngbDropdownToggle>
|
||||||
>
|
<span class="caret" aria-haspopup="menu" role="button"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div role="menu" ngbDropdownMenu>
|
||||||
|
<h6 class="dropdown-header" i18n>Filter reports</h6>
|
||||||
|
|
||||||
|
<!-- TODO:
|
||||||
|
<div class="dropdown-item" i18n>Reports opened by admins</div>
|
||||||
|
<div class="dropdown-item" i18n>Reports on videos with multiple reports</div>
|
||||||
|
<div class="dropdown-item" i18n>Unassigned reports</div>
|
||||||
|
-->
|
||||||
|
<a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:pending' }" class="dropdown-item" i18n>Unsolved reports</a>
|
||||||
|
<a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:accepted' }" class="dropdown-item" i18n>Accepted reports</a>
|
||||||
|
<a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:rejected' }" class="dropdown-item" i18n>Refused reports</a>
|
||||||
|
<a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'is:blocked' }" class="dropdown-item" i18n>Reports with blocked videos</a>
|
||||||
|
<a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'is:deleted' }" class="dropdown-item" i18n>Reports with deleted videos</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
|
||||||
|
(keyup)="onSearch($event)"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@ -100,7 +122,7 @@
|
||||||
|
|
||||||
<td class="action-cell">
|
<td class="action-cell">
|
||||||
<my-action-dropdown
|
<my-action-dropdown
|
||||||
[ngClass]="{ 'show': expanded }" placement="bottom-right auto" container="body"
|
[ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body"
|
||||||
i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"
|
i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"
|
||||||
></my-action-dropdown>
|
></my-action-dropdown>
|
||||||
</td>
|
</td>
|
||||||
|
@ -118,7 +140,7 @@
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<span class="col-3 moderation-expanded-label" i18n>Reporter</span>
|
<span class="col-3 moderation-expanded-label" i18n>Reporter</span>
|
||||||
<span class="col-9 moderation-expanded-text">
|
<span class="col-9 moderation-expanded-text">
|
||||||
<div class="chip">
|
<a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reporter:"' + videoAbuse.reporterAccount.displayName + '"' }" class="chip">
|
||||||
<img
|
<img
|
||||||
class="avatar"
|
class="avatar"
|
||||||
[src]="videoAbuse.reporterAccount.avatar.path"
|
[src]="videoAbuse.reporterAccount.avatar.path"
|
||||||
|
@ -128,8 +150,8 @@
|
||||||
<div>
|
<div>
|
||||||
<span class="text-muted">{{ createByString(videoAbuse.reporterAccount) }}</span>
|
<span class="text-muted">{{ createByString(videoAbuse.reporterAccount) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</a>
|
||||||
<a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': videoAbuse.reporterAccount.displayName }" class="ml-auto text-muted video-details-links" i18n>
|
<a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reportee:"' + videoAbuse.reporterAccount.displayName + '"' }" class="ml-auto text-muted video-details-links" i18n>
|
||||||
{videoAbuse.countReportsForReporter, plural, =1 {1 report} other {{{ videoAbuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
|
{videoAbuse.countReportsForReporter, plural, =1 {1 report} other {{{ videoAbuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
@ -149,7 +171,7 @@
|
||||||
<span class="text-muted">{{ videoAbuse.video.channel.ownerAccount ? createByString(videoAbuse.video.channel.ownerAccount) : '' }}</span>
|
<span class="text-muted">{{ videoAbuse.video.channel.ownerAccount ? createByString(videoAbuse.video.channel.ownerAccount) : '' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': videoAbuse.video.channel.ownerAccount.displayName }" class="ml-auto text-muted video-details-links" i18n>
|
<a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reportee:"' +videoAbuse.video.channel.ownerAccount.displayName + '"' }" class="ml-auto text-muted video-details-links" i18n>
|
||||||
{videoAbuse.countReportsForReportee, plural, =1 {1 report} other {{{ videoAbuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
|
{videoAbuse.countReportsForReportee, plural, =1 {1 report} other {{{ videoAbuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -13,3 +13,11 @@
|
||||||
.video-abuse-states .glyphicon-comment {
|
.video-abuse-states .glyphicon-comment {
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
@include peertube-input-group(300px);
|
||||||
|
|
||||||
|
.dropdown-toggle::after {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -16,7 +16,3 @@ input[type=submit] {
|
||||||
|
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-group-append {
|
|
||||||
height: 30px;
|
|
||||||
}
|
|
||||||
|
|
|
@ -19,10 +19,6 @@ my-actor-avatar-info {
|
||||||
@include peertube-input-group(fit-content);
|
@include peertube-input-group(fit-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-group-append {
|
|
||||||
height: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
input {
|
||||||
&[type=text] {
|
&[type=text] {
|
||||||
@include peertube-input-text(340px);
|
@include peertube-input-text(340px);
|
||||||
|
|
|
@ -58,10 +58,6 @@
|
||||||
@include peertube-input-group(400px);
|
@include peertube-input-group(400px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-group-append {
|
|
||||||
height: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:not([type=submit]) {
|
input:not([type=submit]) {
|
||||||
@include peertube-input-text(400px);
|
@include peertube-input-text(400px);
|
||||||
|
|
||||||
|
|
|
@ -88,10 +88,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-header {
|
|
||||||
padding-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
::ng-deep form {
|
::ng-deep form {
|
||||||
padding: 0.25rem 1rem;
|
padding: 0.25rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,10 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
|
||||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
|
||||||
|
.dropdown-header {
|
||||||
|
padding-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.dropdown-item {
|
.dropdown-item {
|
||||||
padding: 3px 15px;
|
padding: 3px 15px;
|
||||||
|
|
||||||
|
@ -262,6 +266,18 @@ ngb-tooltip-window {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-group > .form-control {
|
.input-group {
|
||||||
flex: initial;
|
& > .form-control {
|
||||||
|
flex: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group-prepend,
|
||||||
|
.input-group-append {
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group-prepend + input {
|
||||||
|
border-top-left-radius: 0 !important;
|
||||||
|
border-bottom-left-radius: 0 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,10 @@ p-table {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
.input-group-text {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -219,6 +219,54 @@ function searchAttribute (sourceField, targetField) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface QueryStringFilterPrefixes {
|
||||||
|
[key: string]: string | { prefix: string, handler: Function, multiple?: boolean }
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseQueryStringFilter (q: string, prefixes: QueryStringFilterPrefixes) {
|
||||||
|
const tokens = q // tokenize only if we have a querystring
|
||||||
|
? [].concat.apply([], q.split('"').map((v, i) => i % 2 ? v : v.split(' '))).filter(Boolean)
|
||||||
|
: []
|
||||||
|
|
||||||
|
// TODO: when Typescript supports Object.fromEntries, replace with the Object method
|
||||||
|
function fromEntries<T> (entries: [keyof T, T[keyof T]][]): T {
|
||||||
|
return entries.reduce(
|
||||||
|
(acc, [ key, value ]) => ({ ...acc, [key]: value }),
|
||||||
|
{} as T
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectMap = (obj, fn) => fromEntries(
|
||||||
|
Object.entries(obj).map(
|
||||||
|
([ k, v ], i) => [ k, fn(v, k, i) ]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
// search is the querystring minus defined filters
|
||||||
|
search: tokens.filter(e => !Object.values(prefixes).some(p => {
|
||||||
|
if (typeof p === "string") {
|
||||||
|
return e.startsWith(p)
|
||||||
|
} else {
|
||||||
|
return e.startsWith(p.prefix)
|
||||||
|
}
|
||||||
|
})).join(' '),
|
||||||
|
// filters defined in prefixes are added under their own name
|
||||||
|
...objectMap(prefixes, v => {
|
||||||
|
if (typeof v === "string") {
|
||||||
|
return tokens.filter(e => e.startsWith(v)).map(e => e.slice(v.length))
|
||||||
|
} else {
|
||||||
|
const _tokens = tokens.filter(e => e.startsWith(v.prefix)).map(e => e.slice(v.prefix.length)).map(v.handler)
|
||||||
|
return !v.multiple
|
||||||
|
? _tokens.length > 0
|
||||||
|
? _tokens[0]
|
||||||
|
: ''
|
||||||
|
: _tokens
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -241,7 +289,8 @@ export {
|
||||||
getFollowsSort,
|
getFollowsSort,
|
||||||
buildDirectionAndField,
|
buildDirectionAndField,
|
||||||
createSafeIn,
|
createSafeIn,
|
||||||
searchAttribute
|
searchAttribute,
|
||||||
|
parseQueryStringFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
isVideoAbuseStateValid
|
isVideoAbuseStateValid
|
||||||
} from '../../helpers/custom-validators/video-abuses'
|
} from '../../helpers/custom-validators/video-abuses'
|
||||||
import { AccountModel } from '../account/account'
|
import { AccountModel } from '../account/account'
|
||||||
import { buildBlockedAccountSQL, getSort, throwIfNotValid, searchAttribute } from '../utils'
|
import { buildBlockedAccountSQL, getSort, throwIfNotValid, searchAttribute, parseQueryStringFilter } from '../utils'
|
||||||
import { VideoModel } from './video'
|
import { VideoModel } from './video'
|
||||||
import { VideoAbuseState, VideoDetails } from '../../../shared'
|
import { VideoAbuseState, VideoDetails } from '../../../shared'
|
||||||
import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
|
import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
|
||||||
|
@ -26,10 +26,17 @@ export enum ScopeNames {
|
||||||
|
|
||||||
@Scopes(() => ({
|
@Scopes(() => ({
|
||||||
[ScopeNames.FOR_API]: (options: {
|
[ScopeNames.FOR_API]: (options: {
|
||||||
|
// search
|
||||||
search?: string
|
search?: string
|
||||||
searchReporter?: string
|
searchReporter?: string
|
||||||
|
searchReportee?: string
|
||||||
searchVideo?: string
|
searchVideo?: string
|
||||||
searchVideoChannel?: string
|
searchVideoChannel?: string
|
||||||
|
// filters
|
||||||
|
id?: number
|
||||||
|
state?: VideoAbuseState
|
||||||
|
is?: any
|
||||||
|
// accountIds
|
||||||
serverAccountId: number
|
serverAccountId: number
|
||||||
userAccountId: number
|
userAccountId: number
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -71,6 +78,24 @@ export enum ScopeNames {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.id) {
|
||||||
|
where = Object.assign(where, {
|
||||||
|
id: options.id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.state) {
|
||||||
|
where = Object.assign(where, {
|
||||||
|
state: options.state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.is) {
|
||||||
|
where = Object.assign(where, {
|
||||||
|
...options.is
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
attributes: {
|
attributes: {
|
||||||
include: [
|
include: [
|
||||||
|
@ -167,7 +192,13 @@ export enum ScopeNames {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }),
|
model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }),
|
||||||
where: searchAttribute(options.searchVideoChannel, 'name')
|
where: searchAttribute(options.searchVideoChannel, 'name'),
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: AccountModel,
|
||||||
|
where: searchAttribute(options.searchReportee, 'name')
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
attributes: [ 'id', 'reason', 'unfederated' ],
|
attributes: [ 'id', 'reason', 'unfederated' ],
|
||||||
|
@ -280,7 +311,36 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const filters = {
|
const filters = {
|
||||||
search,
|
...parseQueryStringFilter(search, {
|
||||||
|
id: {
|
||||||
|
prefix: '#',
|
||||||
|
handler: v => v
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
prefix: 'state:',
|
||||||
|
handler: v => {
|
||||||
|
if (v === "accepted") return VideoAbuseState.ACCEPTED
|
||||||
|
if (v === "pending") return VideoAbuseState.PENDING
|
||||||
|
if (v === "rejected") return VideoAbuseState.REJECTED
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
is: {
|
||||||
|
prefix: 'is:',
|
||||||
|
handler: v => {
|
||||||
|
if (v === "deleted") return { deletedVideo: { [Op.not]: null } }
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
searchReporter: {
|
||||||
|
prefix: 'reporter:',
|
||||||
|
handler: v => v
|
||||||
|
},
|
||||||
|
searchReportee: {
|
||||||
|
prefix: 'reportee:',
|
||||||
|
handler: v => v
|
||||||
|
}
|
||||||
|
}),
|
||||||
serverAccountId,
|
serverAccountId,
|
||||||
userAccountId
|
userAccountId
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue