diff options
11 files changed, 175 insertions, 31 deletions
diff --git a/client/src/app/+admin/moderation/moderation.component.scss b/client/src/app/+admin/moderation/moderation.component.scss index cf06401cf..26c2a30d4 100644 --- a/client/src/app/+admin/moderation/moderation.component.scss +++ b/client/src/app/+admin/moderation/moderation.component.scss | |||
@@ -12,6 +12,7 @@ | |||
12 | 12 | ||
13 | input { | 13 | input { |
14 | @include peertube-input-text(250px); | 14 | @include peertube-input-text(250px); |
15 | flex-grow: 1; | ||
15 | } | 16 | } |
16 | } | 17 | } |
17 | 18 | ||
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html index 2f6e12d1c..b55b18333 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html +++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html | |||
@@ -7,10 +7,32 @@ | |||
7 | <ng-template pTemplate="caption"> | 7 | <ng-template pTemplate="caption"> |
8 | <div class="caption"> | 8 | <div class="caption"> |
9 | <div class="ml-auto"> | 9 | <div class="ml-auto"> |
10 | <input | 10 | <div class="input-group"> |
11 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." | 11 | <div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body"> |
12 | (keyup)="onSearch($event)" | 12 | <div class="input-group-text" ngbDropdownToggle> |
13 | > | 13 | <span class="caret" aria-haspopup="menu" role="button"></span> |
14 | </div> | ||
15 | |||
16 | <div role="menu" ngbDropdownMenu> | ||
17 | <h6 class="dropdown-header" i18n>Filter reports</h6> | ||
18 | |||
19 | <!-- TODO: | ||
20 | <div class="dropdown-item" i18n>Reports opened by admins</div> | ||
21 | <div class="dropdown-item" i18n>Reports on videos with multiple reports</div> | ||
22 | <div class="dropdown-item" i18n>Unassigned reports</div> | ||
23 | --> | ||
24 | <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:pending' }" class="dropdown-item" i18n>Unsolved reports</a> | ||
25 | <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:accepted' }" class="dropdown-item" i18n>Accepted reports</a> | ||
26 | <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:rejected' }" class="dropdown-item" i18n>Refused reports</a> | ||
27 | <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'is:blocked' }" class="dropdown-item" i18n>Reports with blocked videos</a> | ||
28 | <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'is:deleted' }" class="dropdown-item" i18n>Reports with deleted videos</a> | ||
29 | </div> | ||
30 | </div> | ||
31 | <input | ||
32 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." | ||
33 | (keyup)="onSearch($event)" | ||
34 | > | ||
35 | </div> | ||
14 | </div> | 36 | </div> |
15 | </div> | 37 | </div> |
16 | </ng-template> | 38 | </ng-template> |
@@ -100,7 +122,7 @@ | |||
100 | 122 | ||
101 | <td class="action-cell"> | 123 | <td class="action-cell"> |
102 | <my-action-dropdown | 124 | <my-action-dropdown |
103 | [ngClass]="{ 'show': expanded }" placement="bottom-right auto" container="body" | 125 | [ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body" |
104 | i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse" | 126 | i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse" |
105 | ></my-action-dropdown> | 127 | ></my-action-dropdown> |
106 | </td> | 128 | </td> |
@@ -118,7 +140,7 @@ | |||
118 | <div class="d-flex"> | 140 | <div class="d-flex"> |
119 | <span class="col-3 moderation-expanded-label" i18n>Reporter</span> | 141 | <span class="col-3 moderation-expanded-label" i18n>Reporter</span> |
120 | <span class="col-9 moderation-expanded-text"> | 142 | <span class="col-9 moderation-expanded-text"> |
121 | <div class="chip"> | 143 | <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reporter:"' + videoAbuse.reporterAccount.displayName + '"' }" class="chip"> |
122 | <img | 144 | <img |
123 | class="avatar" | 145 | class="avatar" |
124 | [src]="videoAbuse.reporterAccount.avatar.path" | 146 | [src]="videoAbuse.reporterAccount.avatar.path" |
@@ -128,8 +150,8 @@ | |||
128 | <div> | 150 | <div> |
129 | <span class="text-muted">{{ createByString(videoAbuse.reporterAccount) }}</span> | 151 | <span class="text-muted">{{ createByString(videoAbuse.reporterAccount) }}</span> |
130 | </div> | 152 | </div> |
131 | </div> | 153 | </a> |
132 | <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': videoAbuse.reporterAccount.displayName }" class="ml-auto text-muted video-details-links" i18n> | 154 | <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reportee:"' + videoAbuse.reporterAccount.displayName + '"' }" class="ml-auto text-muted video-details-links" i18n> |
133 | {videoAbuse.countReportsForReporter, plural, =1 {1 report} other {{{ videoAbuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span> | 155 | {videoAbuse.countReportsForReporter, plural, =1 {1 report} other {{{ videoAbuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span> |
134 | </a> | 156 | </a> |
135 | </span> | 157 | </span> |
@@ -149,7 +171,7 @@ | |||
149 | <span class="text-muted">{{ videoAbuse.video.channel.ownerAccount ? createByString(videoAbuse.video.channel.ownerAccount) : '' }}</span> | 171 | <span class="text-muted">{{ videoAbuse.video.channel.ownerAccount ? createByString(videoAbuse.video.channel.ownerAccount) : '' }}</span> |
150 | </div> | 172 | </div> |
151 | </div> | 173 | </div> |
152 | <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': videoAbuse.video.channel.ownerAccount.displayName }" class="ml-auto text-muted video-details-links" i18n> | 174 | <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reportee:"' +videoAbuse.video.channel.ownerAccount.displayName + '"' }" class="ml-auto text-muted video-details-links" i18n> |
153 | {videoAbuse.countReportsForReportee, plural, =1 {1 report} other {{{ videoAbuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span> | 175 | {videoAbuse.countReportsForReportee, plural, =1 {1 report} other {{{ videoAbuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span> |
154 | </a> | 176 | </a> |
155 | </span> | 177 | </span> |
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss index d6bc34935..8eee15b64 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss +++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss | |||
@@ -13,3 +13,11 @@ | |||
13 | .video-abuse-states .glyphicon-comment { | 13 | .video-abuse-states .glyphicon-comment { |
14 | margin-left: 0.5rem; | 14 | margin-left: 0.5rem; |
15 | } | 15 | } |
16 | |||
17 | .input-group { | ||
18 | @include peertube-input-group(300px); | ||
19 | |||
20 | .dropdown-toggle::after { | ||
21 | margin-left: 0; | ||
22 | } | ||
23 | } | ||
diff --git a/client/src/app/+admin/users/user-edit/user-password.component.scss b/client/src/app/+admin/users/user-edit/user-password.component.scss index 217d585af..5cd93f6af 100644 --- a/client/src/app/+admin/users/user-edit/user-password.component.scss +++ b/client/src/app/+admin/users/user-edit/user-password.component.scss | |||
@@ -16,7 +16,3 @@ input[type=submit] { | |||
16 | 16 | ||
17 | margin-top: 10px; | 17 | margin-top: 10px; |
18 | } | 18 | } |
19 | |||
20 | .input-group-append { | ||
21 | height: 30px; | ||
22 | } | ||
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.scss b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.scss index 8f8af655c..ba27ee7ff 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.scss +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.scss | |||
@@ -19,10 +19,6 @@ my-actor-avatar-info { | |||
19 | @include peertube-input-group(fit-content); | 19 | @include peertube-input-group(fit-content); |
20 | } | 20 | } |
21 | 21 | ||
22 | .input-group-append { | ||
23 | height: 30px; | ||
24 | } | ||
25 | |||
26 | input { | 22 | input { |
27 | &[type=text] { | 23 | &[type=text] { |
28 | @include peertube-input-text(340px); | 24 | @include peertube-input-text(340px); |
diff --git a/client/src/app/+signup/+register/register.component.scss b/client/src/app/+signup/+register/register.component.scss index e135b5cb4..cc60ef524 100644 --- a/client/src/app/+signup/+register/register.component.scss +++ b/client/src/app/+signup/+register/register.component.scss | |||
@@ -58,10 +58,6 @@ | |||
58 | @include peertube-input-group(400px); | 58 | @include peertube-input-group(400px); |
59 | } | 59 | } |
60 | 60 | ||
61 | .input-group-append { | ||
62 | height: 30px; | ||
63 | } | ||
64 | |||
65 | input:not([type=submit]) { | 61 | input:not([type=submit]) { |
66 | @include peertube-input-text(400px); | 62 | @include peertube-input-text(400px); |
67 | 63 | ||
diff --git a/client/src/app/shared/user-subscription/subscribe-button.component.scss b/client/src/app/shared/user-subscription/subscribe-button.component.scss index 5283a6cc3..b739c5ae2 100644 --- a/client/src/app/shared/user-subscription/subscribe-button.component.scss +++ b/client/src/app/shared/user-subscription/subscribe-button.component.scss | |||
@@ -88,10 +88,6 @@ | |||
88 | } | 88 | } |
89 | } | 89 | } |
90 | 90 | ||
91 | .dropdown-header { | ||
92 | padding-left: 1rem; | ||
93 | } | ||
94 | |||
95 | ::ng-deep form { | 91 | ::ng-deep form { |
96 | padding: 0.25rem 1rem; | 92 | padding: 0.25rem 1rem; |
97 | } | 93 | } |
diff --git a/client/src/sass/bootstrap.scss b/client/src/sass/bootstrap.scss index 377c85070..50f1dafed 100644 --- a/client/src/sass/bootstrap.scss +++ b/client/src/sass/bootstrap.scss | |||
@@ -41,6 +41,10 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/'; | |||
41 | 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); | 41 | 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); |
42 | font-size: 15px; | 42 | font-size: 15px; |
43 | 43 | ||
44 | .dropdown-header { | ||
45 | padding-left: 1rem; | ||
46 | } | ||
47 | |||
44 | .dropdown-item { | 48 | .dropdown-item { |
45 | padding: 3px 15px; | 49 | padding: 3px 15px; |
46 | 50 | ||
@@ -262,6 +266,18 @@ ngb-tooltip-window { | |||
262 | } | 266 | } |
263 | } | 267 | } |
264 | 268 | ||
265 | .input-group > .form-control { | 269 | .input-group { |
266 | flex: initial; | 270 | & > .form-control { |
271 | flex: initial; | ||
272 | } | ||
273 | |||
274 | .input-group-prepend, | ||
275 | .input-group-append { | ||
276 | height: 30px; | ||
277 | } | ||
278 | |||
279 | .input-group-prepend + input { | ||
280 | border-top-left-radius: 0 !important; | ||
281 | border-bottom-left-radius: 0 !important; | ||
282 | } | ||
267 | } | 283 | } |
diff --git a/client/src/sass/primeng-custom.scss b/client/src/sass/primeng-custom.scss index b3cd7cf51..eab2b2dfd 100644 --- a/client/src/sass/primeng-custom.scss +++ b/client/src/sass/primeng-custom.scss | |||
@@ -32,6 +32,10 @@ p-table { | |||
32 | height: 40px; | 32 | height: 40px; |
33 | display: flex; | 33 | display: flex; |
34 | align-items: center; | 34 | align-items: center; |
35 | |||
36 | .input-group-text { | ||
37 | background-color: transparent; | ||
38 | } | ||
35 | } | 39 | } |
36 | } | 40 | } |
37 | 41 | ||
diff --git a/server/models/utils.ts b/server/models/utils.ts index bdf2291f0..3e3825b32 100644 --- a/server/models/utils.ts +++ b/server/models/utils.ts | |||
@@ -219,6 +219,54 @@ function searchAttribute (sourceField, targetField) { | |||
219 | } | 219 | } |
220 | } | 220 | } |
221 | 221 | ||
222 | interface QueryStringFilterPrefixes { | ||
223 | [key: string]: string | { prefix: string, handler: Function, multiple?: boolean } | ||
224 | } | ||
225 | |||
226 | function parseQueryStringFilter (q: string, prefixes: QueryStringFilterPrefixes) { | ||
227 | const tokens = q // tokenize only if we have a querystring | ||
228 | ? [].concat.apply([], q.split('"').map((v, i) => i % 2 ? v : v.split(' '))).filter(Boolean) | ||
229 | : [] | ||
230 | |||
231 | // TODO: when Typescript supports Object.fromEntries, replace with the Object method | ||
232 | function fromEntries<T> (entries: [keyof T, T[keyof T]][]): T { | ||
233 | return entries.reduce( | ||
234 | (acc, [ key, value ]) => ({ ...acc, [key]: value }), | ||
235 | {} as T | ||
236 | ) | ||
237 | } | ||
238 | |||
239 | const objectMap = (obj, fn) => fromEntries( | ||
240 | Object.entries(obj).map( | ||
241 | ([ k, v ], i) => [ k, fn(v, k, i) ] | ||
242 | ) | ||
243 | ) | ||
244 | |||
245 | return { | ||
246 | // search is the querystring minus defined filters | ||
247 | search: tokens.filter(e => !Object.values(prefixes).some(p => { | ||
248 | if (typeof p === "string") { | ||
249 | return e.startsWith(p) | ||
250 | } else { | ||
251 | return e.startsWith(p.prefix) | ||
252 | } | ||
253 | })).join(' '), | ||
254 | // filters defined in prefixes are added under their own name | ||
255 | ...objectMap(prefixes, v => { | ||
256 | if (typeof v === "string") { | ||
257 | return tokens.filter(e => e.startsWith(v)).map(e => e.slice(v.length)) | ||
258 | } else { | ||
259 | const _tokens = tokens.filter(e => e.startsWith(v.prefix)).map(e => e.slice(v.prefix.length)).map(v.handler) | ||
260 | return !v.multiple | ||
261 | ? _tokens.length > 0 | ||
262 | ? _tokens[0] | ||
263 | : '' | ||
264 | : _tokens | ||
265 | } | ||
266 | }) | ||
267 | } | ||
268 | } | ||
269 | |||
222 | // --------------------------------------------------------------------------- | 270 | // --------------------------------------------------------------------------- |
223 | 271 | ||
224 | export { | 272 | export { |
@@ -241,7 +289,8 @@ export { | |||
241 | getFollowsSort, | 289 | getFollowsSort, |
242 | buildDirectionAndField, | 290 | buildDirectionAndField, |
243 | createSafeIn, | 291 | createSafeIn, |
244 | searchAttribute | 292 | searchAttribute, |
293 | parseQueryStringFilter | ||
245 | } | 294 | } |
246 | 295 | ||
247 | // --------------------------------------------------------------------------- | 296 | // --------------------------------------------------------------------------- |
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts index 628f1caa6..b1f8fed90 100644 --- a/server/models/video/video-abuse.ts +++ b/server/models/video/video-abuse.ts | |||
@@ -9,7 +9,7 @@ import { | |||
9 | isVideoAbuseStateValid | 9 | isVideoAbuseStateValid |
10 | } from '../../helpers/custom-validators/video-abuses' | 10 | } from '../../helpers/custom-validators/video-abuses' |
11 | import { AccountModel } from '../account/account' | 11 | import { AccountModel } from '../account/account' |
12 | import { buildBlockedAccountSQL, getSort, throwIfNotValid, searchAttribute } from '../utils' | 12 | import { buildBlockedAccountSQL, getSort, throwIfNotValid, searchAttribute, parseQueryStringFilter } from '../utils' |
13 | import { VideoModel } from './video' | 13 | import { VideoModel } from './video' |
14 | import { VideoAbuseState, VideoDetails } from '../../../shared' | 14 | import { VideoAbuseState, VideoDetails } from '../../../shared' |
15 | import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants' | 15 | import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants' |
@@ -26,10 +26,17 @@ export enum ScopeNames { | |||
26 | 26 | ||
27 | @Scopes(() => ({ | 27 | @Scopes(() => ({ |
28 | [ScopeNames.FOR_API]: (options: { | 28 | [ScopeNames.FOR_API]: (options: { |
29 | // search | ||
29 | search?: string | 30 | search?: string |
30 | searchReporter?: string | 31 | searchReporter?: string |
32 | searchReportee?: string | ||
31 | searchVideo?: string | 33 | searchVideo?: string |
32 | searchVideoChannel?: string | 34 | searchVideoChannel?: string |
35 | // filters | ||
36 | id?: number | ||
37 | state?: VideoAbuseState | ||
38 | is?: any | ||
39 | // accountIds | ||
33 | serverAccountId: number | 40 | serverAccountId: number |
34 | userAccountId: number | 41 | userAccountId: number |
35 | }) => { | 42 | }) => { |
@@ -71,6 +78,24 @@ export enum ScopeNames { | |||
71 | }) | 78 | }) |
72 | } | 79 | } |
73 | 80 | ||
81 | if (options.id) { | ||
82 | where = Object.assign(where, { | ||
83 | id: options.id | ||
84 | }) | ||
85 | } | ||
86 | |||
87 | if (options.state) { | ||
88 | where = Object.assign(where, { | ||
89 | state: options.state | ||
90 | }) | ||
91 | } | ||
92 | |||
93 | if (options.is) { | ||
94 | where = Object.assign(where, { | ||
95 | ...options.is | ||
96 | }) | ||
97 | } | ||
98 | |||
74 | return { | 99 | return { |
75 | attributes: { | 100 | attributes: { |
76 | include: [ | 101 | include: [ |
@@ -167,7 +192,13 @@ export enum ScopeNames { | |||
167 | }, | 192 | }, |
168 | { | 193 | { |
169 | model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }), | 194 | model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }), |
170 | where: searchAttribute(options.searchVideoChannel, 'name') | 195 | where: searchAttribute(options.searchVideoChannel, 'name'), |
196 | include: [ | ||
197 | { | ||
198 | model: AccountModel, | ||
199 | where: searchAttribute(options.searchReportee, 'name') | ||
200 | } | ||
201 | ] | ||
171 | }, | 202 | }, |
172 | { | 203 | { |
173 | attributes: [ 'id', 'reason', 'unfederated' ], | 204 | attributes: [ 'id', 'reason', 'unfederated' ], |
@@ -280,7 +311,36 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> { | |||
280 | } | 311 | } |
281 | 312 | ||
282 | const filters = { | 313 | const filters = { |
283 | search, | 314 | ...parseQueryStringFilter(search, { |
315 | id: { | ||
316 | prefix: '#', | ||
317 | handler: v => v | ||
318 | }, | ||
319 | state: { | ||
320 | prefix: 'state:', | ||
321 | handler: v => { | ||
322 | if (v === "accepted") return VideoAbuseState.ACCEPTED | ||
323 | if (v === "pending") return VideoAbuseState.PENDING | ||
324 | if (v === "rejected") return VideoAbuseState.REJECTED | ||
325 | return undefined | ||
326 | } | ||
327 | }, | ||
328 | is: { | ||
329 | prefix: 'is:', | ||
330 | handler: v => { | ||
331 | if (v === "deleted") return { deletedVideo: { [Op.not]: null } } | ||
332 | return undefined | ||
333 | } | ||
334 | }, | ||
335 | searchReporter: { | ||
336 | prefix: 'reporter:', | ||
337 | handler: v => v | ||
338 | }, | ||
339 | searchReportee: { | ||
340 | prefix: 'reportee:', | ||
341 | handler: v => v | ||
342 | } | ||
343 | }), | ||
284 | serverAccountId, | 344 | serverAccountId, |
285 | userAccountId | 345 | userAccountId |
286 | } | 346 | } |