@@ -3,9 +3,9 @@ import type { SpamUserQueryOrdering } from 'types';
33import type { SpamUsersFilter } from './filters' ;
44import type { SpamUser } from './types' ;
55
6- import React , { useCallback , useState } from 'react' ;
6+ import React , { useCallback , useMemo , useState } from 'react' ;
77
8- import { HTMLSelect , Spinner } from '@blueprintjs/core' ;
8+ import { Button , ButtonGroup , HTMLSelect , Spinner } from '@blueprintjs/core' ;
99import { useDebounce , useUpdateEffect } from 'react-use' ;
1010
1111import { OverviewSearchGroup } from 'client/containers/DashboardOverview/helpers' ;
@@ -25,12 +25,38 @@ type Props = {
2525const sortOptions = [
2626 { value : 'user-created-at:DESC' , label : 'Newest users' } ,
2727 { value : 'user-created-at:ASC' , label : 'Oldest users' } ,
28+ { value : 'last-activity:DESC' , label : 'Most recently active' } ,
29+ { value : 'last-activity:ASC' , label : 'Least recently active' } ,
30+ { value : 'activity-count:DESC' , label : 'Most activities' } ,
31+ { value : 'activity-count:ASC' , label : 'Fewest activities' } ,
2832 { value : 'discussion-count:DESC' , label : 'Most discussions' } ,
2933 { value : 'discussion-count:ASC' , label : 'Fewest discussions' } ,
3034 { value : 'spam-score:DESC' , label : 'Highest spam score' } ,
3135 { value : 'spam-score:ASC' , label : 'Lowest spam score' } ,
3236] ;
3337
38+ type DatePreset = { label : string ; days : number } ;
39+
40+ const datePresets : DatePreset [ ] = [
41+ { label : '24h' , days : 1 } ,
42+ { label : '7d' , days : 7 } ,
43+ { label : '30d' , days : 30 } ,
44+ { label : '90d' , days : 90 } ,
45+ ] ;
46+
47+ const daysAgo = ( n : number ) : string => {
48+ const d = new Date ( ) ;
49+ d . setDate ( d . getDate ( ) - n ) ;
50+ return d . toISOString ( ) ;
51+ } ;
52+
53+ const toDateInputValue = ( iso : string ) : string => iso . slice ( 0 , 10 ) ;
54+
55+ const fromDateInputValue = ( val : string ) : string | undefined => {
56+ if ( ! val ) return undefined ;
57+ return new Date ( val ) . toISOString ( ) ;
58+ } ;
59+
3460const parseSort = ( value : string ) : SpamUserQueryOrdering => {
3561 const [ field , direction ] = value . split ( ':' ) ;
3662 return { field, direction } as SpamUserQueryOrdering ;
@@ -46,9 +72,41 @@ const UserSpam = (props: Props) => {
4672 ) ;
4773 const [ communityInput , setCommunityInput ] = useState ( '' ) ;
4874 const [ communitySubdomain , setCommunitySubdomain ] = useState ( '' ) ;
75+ const [ createdAfter , setCreatedAfter ] = useState < string | undefined > ( ) ;
76+ const [ createdBefore , setCreatedBefore ] = useState < string | undefined > ( ) ;
77+ const [ activeAfter , setActiveAfter ] = useState < string | undefined > ( ) ;
78+ const [ activeBefore , setActiveBefore ] = useState < string | undefined > ( ) ;
79+ const [ createdPreset , setCreatedPreset ] = useState < number | null > ( null ) ;
80+ const [ activePreset , setActivePreset ] = useState < number | null > ( null ) ;
81+ const [ minActivitiesInput , setMinActivitiesInput ] = useState ( '' ) ;
82+ const [ maxActivitiesInput , setMaxActivitiesInput ] = useState ( '' ) ;
83+ const [ minActivities , setMinActivities ] = useState < number | undefined > ( ) ;
84+ const [ maxActivities , setMaxActivities ] = useState < number | undefined > ( ) ;
4985
5086 useDebounce ( ( ) => setSearchTerm ( inputSearchTerm ) , 300 , [ inputSearchTerm ] ) ;
5187 useDebounce ( ( ) => setCommunitySubdomain ( communityInput . trim ( ) ) , 300 , [ communityInput ] ) ;
88+ useDebounce (
89+ ( ) => setMinActivities ( minActivitiesInput ? Number ( minActivitiesInput ) : undefined ) ,
90+ 300 ,
91+ [ minActivitiesInput ] ,
92+ ) ;
93+ useDebounce (
94+ ( ) => setMaxActivities ( maxActivitiesInput ? Number ( maxActivitiesInput ) : undefined ) ,
95+ 300 ,
96+ [ maxActivitiesInput ] ,
97+ ) ;
98+
99+ const queryFilters = useMemo (
100+ ( ) => ( {
101+ createdAfter,
102+ createdBefore,
103+ activeAfter,
104+ activeBefore,
105+ minActivities,
106+ maxActivities,
107+ } ) ,
108+ [ createdAfter , createdBefore , activeAfter , activeBefore , minActivities , maxActivities ] ,
109+ ) ;
52110
53111 const { users, isLoading, loadMoreUsers, mayLoadMoreUsers, updateUser } = useSpamUsers ( {
54112 limit : 50 ,
@@ -57,10 +115,11 @@ const UserSpam = (props: Props) => {
57115 filter,
58116 ordering,
59117 communitySubdomain : communitySubdomain || undefined ,
118+ queryFilters,
60119 } ) ;
61120
62121 useInfiniteScroll ( {
63- scrollTolerance : 100 ,
122+ scrollTolerance : 500 ,
64123 useDocumentElement : true ,
65124 onRequestMoreItems : loadMoreUsers ,
66125 enabled : mayLoadMoreUsers ,
@@ -78,6 +137,15 @@ const UserSpam = (props: Props) => {
78137 [ updateUser ] ,
79138 ) ;
80139
140+ const handleStatusChanged = useCallback (
141+ ( userId : string , status : string ) => {
142+ updateUser ( userId , {
143+ spamTag : { status } as SpamUser [ 'spamTag' ] ,
144+ } as Partial < SpamUser > ) ;
145+ } ,
146+ [ updateUser ] ,
147+ ) ;
148+
81149 const handleFilterChange = useCallback ( ( newFilter : SpamUsersFilter ) => {
82150 setFilter ( newFilter ) ;
83151 setOrdering ( newFilter . query ! . ordering ) ;
@@ -127,13 +195,142 @@ const UserSpam = (props: Props) => {
127195 onChange = { ( e ) => setCommunityInput ( e . target . value ) }
128196 />
129197 </ label >
198+ < span className = "activity-count-filter" >
199+ Activities
200+ < input
201+ type = "number"
202+ min = "0"
203+ placeholder = "min"
204+ value = { minActivitiesInput }
205+ onChange = { ( e ) => setMinActivitiesInput ( e . target . value ) }
206+ />
207+ < span > to</ span >
208+ < input
209+ type = "number"
210+ min = "0"
211+ placeholder = "max"
212+ value = { maxActivitiesInput }
213+ onChange = { ( e ) => setMaxActivitiesInput ( e . target . value ) }
214+ />
215+ </ span >
216+ </ div >
217+ < div className = "date-filters-row" >
218+ < div className = "date-filter-group" >
219+ < span className = "date-filter-label" > Created</ span >
220+ < ButtonGroup minimal >
221+ { datePresets . map ( ( p ) => (
222+ < Button
223+ key = { p . label }
224+ small
225+ active = { createdPreset === p . days }
226+ onClick = { ( ) => {
227+ if ( createdPreset === p . days ) {
228+ setCreatedPreset ( null ) ;
229+ setCreatedAfter ( undefined ) ;
230+ setCreatedBefore ( undefined ) ;
231+ return ;
232+ }
233+ setCreatedPreset ( p . days ) ;
234+ setCreatedAfter ( daysAgo ( p . days ) ) ;
235+ setCreatedBefore ( undefined ) ;
236+ } }
237+ >
238+ { p . label }
239+ </ Button >
240+ ) ) }
241+ </ ButtonGroup >
242+ < input
243+ type = "date"
244+ value = { createdAfter ? toDateInputValue ( createdAfter ) : '' }
245+ onChange = { ( e ) => {
246+ setCreatedPreset ( null ) ;
247+ setCreatedAfter ( fromDateInputValue ( e . target . value ) ) ;
248+ } }
249+ />
250+ < span > to</ span >
251+ < input
252+ type = "date"
253+ value = { createdBefore ? toDateInputValue ( createdBefore ) : '' }
254+ onChange = { ( e ) => {
255+ setCreatedPreset ( null ) ;
256+ setCreatedBefore ( fromDateInputValue ( e . target . value ) ) ;
257+ } }
258+ />
259+ { ( createdAfter || createdBefore ) && (
260+ < Button
261+ small
262+ minimal
263+ icon = "cross"
264+ onClick = { ( ) => {
265+ setCreatedPreset ( null ) ;
266+ setCreatedAfter ( undefined ) ;
267+ setCreatedBefore ( undefined ) ;
268+ } }
269+ />
270+ ) }
271+ </ div >
272+ < div className = "date-filter-group" >
273+ < span className = "date-filter-label" > Active</ span >
274+ < ButtonGroup minimal >
275+ { datePresets . map ( ( p ) => (
276+ < Button
277+ key = { p . label }
278+ small
279+ active = { activePreset === p . days }
280+ onClick = { ( ) => {
281+ if ( activePreset === p . days ) {
282+ setActivePreset ( null ) ;
283+ setActiveAfter ( undefined ) ;
284+ setActiveBefore ( undefined ) ;
285+ return ;
286+ }
287+ setActivePreset ( p . days ) ;
288+ setActiveAfter ( daysAgo ( p . days ) ) ;
289+ setActiveBefore ( undefined ) ;
290+ } }
291+ >
292+ { p . label }
293+ </ Button >
294+ ) ) }
295+ </ ButtonGroup >
296+ < input
297+ type = "date"
298+ value = { activeAfter ? toDateInputValue ( activeAfter ) : '' }
299+ onChange = { ( e ) => {
300+ setActivePreset ( null ) ;
301+ setActiveAfter ( fromDateInputValue ( e . target . value ) ) ;
302+ } }
303+ />
304+ < span > to</ span >
305+ < input
306+ type = "date"
307+ value = { activeBefore ? toDateInputValue ( activeBefore ) : '' }
308+ onChange = { ( e ) => {
309+ setActivePreset ( null ) ;
310+ setActiveBefore ( fromDateInputValue ( e . target . value ) ) ;
311+ } }
312+ />
313+ { ( activeAfter || activeBefore ) && (
314+ < Button
315+ small
316+ minimal
317+ icon = "cross"
318+ onClick = { ( ) => {
319+ setActivePreset ( null ) ;
320+ setActiveAfter ( undefined ) ;
321+ setActiveBefore ( undefined ) ;
322+ } }
323+ />
324+ ) }
325+ </ div >
130326 </ div >
131327 < div className = "users" >
132328 { users . map ( ( user ) => (
133329 < UserSpamEntry
134330 user = { user }
135331 key = { user . id }
136332 onSpamTagRemoved = { handleSpamTagRemoved }
333+ onStatusChanged = { handleStatusChanged }
137334 />
138335 ) ) }
139336 { ! isLoading && users . length === 0 && (
0 commit comments