From da9e7a7f78002758a2d31e58019caf5ccc3dce86 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Fri, 2 Feb 2024 12:56:10 +0100 Subject: [PATCH 1/6] :sparkles: Add advanced filters for event stream --- components/mod-event/EventList.tsx | 303 ++++++++++++++++++----- components/mod-event/useModEventList.tsx | 119 ++++++++- package.json | 2 +- yarn.lock | 93 +------ 4 files changed, 360 insertions(+), 157 deletions(-) diff --git a/components/mod-event/EventList.tsx b/components/mod-event/EventList.tsx index cdd9f23f..fe7e18f0 100644 --- a/components/mod-event/EventList.tsx +++ b/components/mod-event/EventList.tsx @@ -1,14 +1,19 @@ -import { ModEventListQueryOptions, useModEventList } from './useModEventList' +import { + FIRST_EVENT_TIMESTAMP, + ModEventListQueryOptions, + useModEventList, +} from './useModEventList' import { LoadMoreButton } from '@/common/LoadMoreButton' import { ModEventItem } from './EventItem' import { Dropdown } from '@/common/Dropdown' import { MOD_EVENT_TITLES } from './constants' -import { - ArchiveBoxXMarkIcon, - CheckIcon, - ChevronDownIcon, -} from '@heroicons/react/20/solid' +import { ArchiveBoxXMarkIcon, ChevronDownIcon } from '@heroicons/react/20/solid' import { getSubjectTitle } from './helpers/subject' +import { useState } from 'react' +import { Checkbox, FormLabel, Input } from '@/common/forms' +import { ActionButton } from '@/common/buttons' +import { FunnelIcon as FunnelEmptyIcon } from '@heroicons/react/24/outline' +import { FunnelIcon as FunnelFilledIcon } from '@heroicons/react/24/solid' const Header = ({ subjectTitle, @@ -66,7 +71,23 @@ export const ModEventList = ( fetchMoreModEvents, hasMoreModEvents, isInitialLoadingModEvents, + hasFilter, + commentFilter, + toggleCommentFilter, + setCommentFilterKeyword, + createdBy, + setCreatedBy, + subject, + setSubject, + oldestFirst, + setOldestFirst, + createdAfter, + setCreatedAfter, + createdBefore, + setCreatedBefore, } = useModEventList(props) + + const [showFiltersPanel, setShowFiltersPanel] = useState(false) const isEntireHistoryView = !props.subject && !props.createdBy const subjectTitle = getSubjectTitle(modEvents?.[0]?.subject) const noEvents = modEvents.length === 0 && !isInitialLoadingModEvents @@ -86,8 +107,161 @@ export const ModEventList = ( ) : (

Moderation event stream

)} - + setShowFiltersPanel((current) => !current)} + > + {hasFilter ? ( + + ) : ( + + )} + Configure + + {showFiltersPanel && ( +
+
+
+ +
+
+
Comment/Note
+ +
+ toggleCommentFilter()} + className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600" + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault() // make sure we don't submit the form + e.currentTarget.click() // simulate a click on the input + } + }} + /> + +
+ {commentFilter?.enabled && ( + + setCommentFilterKeyword(ev.target.value)} + autoComplete="off" + /> + + )} + + + setCreatedBy(ev.target.value)} + autoComplete="off" + /> + + + + setSubject(ev.target.value)} + autoComplete="off" + /> + + + + setCreatedAfter(ev.target.value)} + autoComplete="off" + min={FIRST_EVENT_TIMESTAMP} + max={new Date().toISOString().split('.')[0]} + /> + + + + setCreatedBefore(ev.target.value)} + autoComplete="off" + min={FIRST_EVENT_TIMESTAMP} + max={new Date().toISOString().split('.')[0]} + /> + +
+
+
+
Sort Direction
+ + setOldestFirst((current) => !current)} + label="Show oldest events first (default: newest first)" + /> +
+
+ )}
{noEvents ? (
@@ -126,70 +300,83 @@ export const ModEventList = ( ) } -const TypeFilter = ({ +// This is component which renders a list of checkboxes by iterating over MOD_EVENT_TITLES list for filtering by type +// same as the TypeFilter component but instead of dropdown, use checkboxes +// on each checkbox click, it will call the `setSelectedTypes` function and toggle the checkox's type +const TypeFilterCheckbox = ({ selectedTypes, setSelectedTypes, }: { selectedTypes: string[] setSelectedTypes: (type: string[]) => void }) => { + const allTypes = Object.entries(MOD_EVENT_TITLES) const toggleType = (type) => { + if (type === 'all') { + if (selectedTypes.length === allTypes.length) { + setSelectedTypes([]) + } else { + setSelectedTypes(allTypes.map(([type]) => type)) + } + return + } const newTypes = selectedTypes.includes(type) ? selectedTypes.filter((t) => t !== type) : [...selectedTypes, type] setSelectedTypes(newTypes) } - let selectedText = 'Filter by type' - - if (selectedTypes.length === 1) { - selectedText = MOD_EVENT_TITLES[selectedTypes[0]] - } else if (selectedTypes.length > 1) { - selectedText = `${selectedTypes.length} selected` - } - return ( - - {!selectedTypes.length && ( - +
+
Event Type
+
+ toggleType('all')} + className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600" + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault() // make sure we don't submit the form + e.currentTarget.click() // simulate a click on the input + } + }} + /> + +
+ {allTypes.map(([type, title]) => ( +
+ toggleType(type)} + className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600" + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault() // make sure we don't submit the form + e.currentTarget.click() // simulate a click on the input + } + }} + /> + +
+ ))} +
) } diff --git a/components/mod-event/useModEventList.tsx b/components/mod-event/useModEventList.tsx index 692c78d7..837a90ac 100644 --- a/components/mod-event/useModEventList.tsx +++ b/components/mod-event/useModEventList.tsx @@ -1,8 +1,9 @@ import { useInfiniteQuery } from '@tanstack/react-query' import client from '@/lib/client' -import { useContext, useState } from 'react' +import { useContext, useEffect, useState } from 'react' import { AuthContext } from '@/shell/AuthContext' import { ComAtprotoAdminQueryModerationEvents } from '@atproto/api' +import { MOD_EVENT_TITLES } from './constants' export type ModEventListQueryOptions = { queryOptions?: { @@ -10,33 +11,101 @@ export type ModEventListQueryOptions = { } } +type CommentFilter = { + enabled: boolean + keyword: string +} + +export const FIRST_EVENT_TIMESTAMP = '2022-11-01T00:00' +const allTypes = Object.keys(MOD_EVENT_TITLES) + export const useModEventList = ( props: { subject?: string; createdBy?: string } & ModEventListQueryOptions, ) => { const { isLoggedIn } = useContext(AuthContext) - const [types, setTypes] = useState([]) + const [createdAfter, setCreatedAfter] = useState( + FIRST_EVENT_TIMESTAMP, + ) + const [createdBefore, setCreatedBefore] = useState( + new Date().toISOString().split('.')[0], + ) + const [subject, setSubject] = useState(props.subject) + const [createdBy, setCreatedBy] = useState( + props.createdBy, + ) + const [types, setTypes] = useState(allTypes) + const [oldestFirst, setOldestFirst] = useState(false) + const [commentFilter, setCommentFilter] = useState({ + enabled: false, + keyword: '', + }) const [includeAllUserRecords, setIncludeAllUserRecords] = useState(false) + useEffect(() => { + if (props.subject !== subject) { + setSubject(props.subject) + } + }, [props.subject]) + + useEffect(() => { + if (props.createdBy !== createdBy) { + setCreatedBy(props.createdBy) + } + }, [props.createdBy]) + const results = useInfiniteQuery({ enabled: isLoggedIn, - queryKey: ['modEventList', { props, types, includeAllUserRecords }], + queryKey: [ + 'modEventList', + { + createdBy, + subject, + types, + includeAllUserRecords, + commentFilter, + oldestFirst, + createdAfter, + createdBefore, + }, + ], queryFn: async ({ pageParam }) => { const queryParams: ComAtprotoAdminQueryModerationEvents.QueryParams = { cursor: pageParam, includeAllUserRecords, } - if (props.subject?.trim()) { - queryParams.subject = props.subject.trim() + if (subject?.trim()) { + queryParams.subject = subject.trim() + } + + if (createdBy?.trim()) { + queryParams.createdBy = createdBy + } + + if (createdAfter) { + queryParams.createdAfter = new Date(createdAfter).toISOString() } - if (props.createdBy?.trim()) { - queryParams.createdBy = props.createdBy + if (createdBefore) { + queryParams.createdBefore = new Date(createdBefore).toISOString() } - if (types.filter(Boolean).length) { - queryParams.types = types.filter(Boolean) + const filterTypes = types.filter(Boolean) + if (filterTypes.length < allTypes.length && filterTypes.length > 0) { + queryParams.types = allTypes + } + + if (oldestFirst) { + queryParams.sortDirection = 'asc' + } + + if (commentFilter.enabled) { + queryParams.hasComment = true + + if (commentFilter.keyword) { + queryParams.commentKeyword = commentFilter.keyword + } } return await getModerationEvents(queryParams) @@ -45,6 +114,15 @@ export const useModEventList = ( ...(props.queryOptions || {}), }) + const hasFilter = + (types.length > 0 && + types.length !== Object.keys(MOD_EVENT_TITLES).length) || + includeAllUserRecords || + commentFilter.enabled || + createdBy || + subject || + oldestFirst + return { types, setTypes, @@ -55,6 +133,29 @@ export const useModEventList = ( hasMoreModEvents: results.hasNextPage, refetchModEvents: results.refetch, isInitialLoadingModEvents: results.isInitialLoading, + hasFilter, + commentFilter, + toggleCommentFilter: () => { + setCommentFilter((prev) => { + if (prev.enabled) { + return { enabled: false, keyword: '' } + } + return { enabled: true, keyword: '' } + }) + }, + setCommentFilterKeyword: (keyword: string) => { + setCommentFilter({ enabled: true, keyword }) + }, + createdBy, + setCreatedBy, + subject, + setSubject, + setOldestFirst, + oldestFirst, + createdBefore, + setCreatedBefore, + createdAfter, + setCreatedAfter, } } diff --git a/package.json b/package.json index 763e98ab..aa725e2d 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "e2e:run": "$(yarn bin)/cypress run" }, "dependencies": { - "@atproto/api": "0.9.2", + "@atproto/api": "link:../atproto/packages/api/dist", "@headlessui/react": "^1.7.7", "@heroicons/react": "^2.0.13", "@tanstack/react-query": "^4.22.0", diff --git a/yarn.lock b/yarn.lock index 1a1a1bb6..5f0604a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,55 +2,9 @@ # yarn lockfile v1 -"@atproto/api@0.9.2": - version "0.9.2" - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.9.2.tgz#acb70a0c0f658eb13b3ee759c700486de820c49e" - integrity sha512-Z1dJM3BER8XsXjw6b6WtbFB1/sf/JXHIGR6tmuOB1AZ1/CLAmBpRN/OAZYz3GD0EHnkzlnjOh9B6FJVfdLKXUQ== - dependencies: - "@atproto/common-web" "^0.2.3" - "@atproto/lexicon" "^0.3.1" - "@atproto/syntax" "^0.1.5" - "@atproto/xrpc" "^0.4.1" - multiformats "^9.9.0" - tlds "^1.234.0" - typed-emitter "^2.1.0" - zod "^3.21.4" - -"@atproto/common-web@^0.2.3": - version "0.2.3" - resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.2.3.tgz#c44c1e177ae8309d5116347d49850209e8e478cc" - integrity sha512-k9VKGYUqjsRlI3wS31XyCbeb2U7ddS4X/eFgzos2CE5rIbk/uQGyKH+0Jcn1JIwRkvI1BemyNuUVrS8Ok3wiuw== - dependencies: - graphemer "^1.4.0" - multiformats "^9.9.0" - uint8arrays "3.0.0" - zod "^3.21.4" - -"@atproto/lexicon@^0.3.1": - version "0.3.1" - resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.3.1.tgz#5d7275d041883a1c930404e3274a6fe7affc151f" - integrity sha512-yLy6GUNP4pn0mGUIyUHvN0UeBza0S03AgjTXVR6KliC4ut2+7SjNMe4cI4G1M8/bJMaccC6ooQSm2kvwiOdr3A== - dependencies: - "@atproto/common-web" "^0.2.3" - "@atproto/syntax" "^0.1.5" - iso-datestring-validator "^2.2.2" - multiformats "^9.9.0" - zod "^3.21.4" - -"@atproto/syntax@^0.1.5": - version "0.1.5" - resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.1.5.tgz#85b6488a33da3b864e8ac22a61b5586b271206ee" - integrity sha512-pbY5lOnThoAbsmrdbN9LC/dNmckfqODJiX9zjW2t3BIHYFeGBc6w9bK3Vre8A0Hg8yWkQpv6gaBLu+ykgi2DJQ== - dependencies: - "@atproto/common-web" "^0.2.3" - -"@atproto/xrpc@^0.4.1": - version "0.4.1" - resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.4.1.tgz#2fb7e81a159b019339bbcdcf4e7ce8dc4e83bef0" - integrity sha512-EMRGiu6oDvFL03Hk2rG/WCL3QK0GjZs9psH80JVf8z2nfdsGON6yn0hw3jvRB26CBXqi58U8Uicyq8Ej5pVTAA== - dependencies: - "@atproto/lexicon" "^0.3.1" - zod "^3.21.4" +"@atproto/api@link:../atproto/packages/api/dist": + version "0.0.0" + uid "" "@babel/runtime-corejs3@^7.10.2": version "7.20.6" @@ -1959,11 +1913,6 @@ grapheme-splitter@^1.0.4: resolved "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz" integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== -graphemer@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" - integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== - has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz" @@ -2425,11 +2374,6 @@ isexe@^2.0.0: resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -iso-datestring-validator@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz#2daa80d2900b7a954f9f731d42f96ee0c19a6895" - integrity sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA== - isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -3155,11 +3099,6 @@ ms@^2.1.1: resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -multiformats@^9.4.2, multiformats@^9.9.0: - version "9.9.0" - resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.9.0.tgz#c68354e7d21037a8f1f8833c8ccd68618e8f1d37" - integrity sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg== - nano-css@^5.3.1: version "5.3.5" resolved "https://registry.yarnpkg.com/nano-css/-/nano-css-5.3.5.tgz#3075ea29ffdeb0c7cb6d25edb21d8f7fa8e8fe8e" @@ -3984,7 +3923,7 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -rxjs@^7.5.1, rxjs@^7.5.2: +rxjs@^7.5.1: version "7.8.1" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== @@ -4365,11 +4304,6 @@ tiny-invariant@^1.2.0: resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== -tlds@^1.234.0: - version "1.248.0" - resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.248.0.tgz#65bf56eee6d0ace1e918fbc653227ef18a9ddf8d" - integrity sha512-noj0KdpWTBhwsKxMOXk0rN9otg4kTgLm4WohERRHbJ9IY+kSDKr3RmjitaQ3JFzny+DyvBOQKlFZhp0G0qNSfg== - tmp@~0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" @@ -4478,25 +4412,11 @@ type-fest@^0.21.3: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== -typed-emitter@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/typed-emitter/-/typed-emitter-2.1.0.tgz#ca78e3d8ef1476f228f548d62e04e3d4d3fd77fb" - integrity sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA== - optionalDependencies: - rxjs "^7.5.2" - typescript@4.9.4: version "4.9.4" resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz" integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== -uint8arrays@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.0.0.tgz#260869efb8422418b6f04e3fac73a3908175c63b" - integrity sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA== - dependencies: - multiformats "^9.4.2" - unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz" @@ -4744,11 +4664,6 @@ yocto-queue@^0.1.0: resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -zod@^3.21.4: - version "3.22.4" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" - integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg== - zwitch@^2.0.0, zwitch@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" From 0a213359745b9e43290fda5455b6fd6ade2611ca Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Fri, 2 Feb 2024 15:02:18 +0100 Subject: [PATCH 2/6] :sparkles: Refactor event list state into a reducer --- components/mod-event/EventList.tsx | 57 ++++++--- components/mod-event/useModEventList.tsx | 155 ++++++++++++++--------- 2 files changed, 135 insertions(+), 77 deletions(-) diff --git a/components/mod-event/EventList.tsx b/components/mod-event/EventList.tsx index fe7e18f0..091cb9ff 100644 --- a/components/mod-event/EventList.tsx +++ b/components/mod-event/EventList.tsx @@ -64,9 +64,7 @@ export const ModEventList = ( ) => { const { types, - setTypes, includeAllUserRecords, - setIncludeAllUserRecords, modEvents, fetchMoreModEvents, hasMoreModEvents, @@ -76,15 +74,12 @@ export const ModEventList = ( toggleCommentFilter, setCommentFilterKeyword, createdBy, - setCreatedBy, subject, - setSubject, oldestFirst, - setOldestFirst, createdAfter, - setCreatedAfter, createdBefore, - setCreatedBefore, + changeListFilter, + resetListFilters } = useModEventList(props) const [showFiltersPanel, setShowFiltersPanel] = useState(false) @@ -100,7 +95,11 @@ export const ModEventList = ( {...{ subjectTitle, includeAllUserRecords, - setIncludeAllUserRecords, + setIncludeAllUserRecords: (value) => + changeListFilter({ + field: 'includeAllUserRecords', + value, + }), isShowingEventsByCreator, }} /> @@ -126,7 +125,9 @@ export const ModEventList = (
+ changeListFilter({ field: 'types', value }) + } />
@@ -188,7 +189,12 @@ export const ModEventList = ( className="block w-full" disabled={!!props.createdBy} value={createdBy || ''} - onChange={(ev) => setCreatedBy(ev.target.value)} + onChange={(ev) => + changeListFilter({ + field: 'createdBy', + value: ev.target.value, + }) + } autoComplete="off" /> @@ -206,7 +212,12 @@ export const ModEventList = ( placeholder="DID or AT-URI" className="block w-full" value={subject || ''} - onChange={(ev) => setSubject(ev.target.value)} + onChange={(ev) => + changeListFilter({ + field: 'subject', + value: ev.target.value, + }) + } autoComplete="off" /> @@ -222,7 +233,12 @@ export const ModEventList = ( name="createdAfter" className="block w-full" value={createdAfter} - onChange={(ev) => setCreatedAfter(ev.target.value)} + onChange={(ev) => + changeListFilter({ + field: 'createdAfter', + value: ev.target.value, + }) + } autoComplete="off" min={FIRST_EVENT_TIMESTAMP} max={new Date().toISOString().split('.')[0]} @@ -240,7 +256,12 @@ export const ModEventList = ( name="createdBefore" className="block w-full" value={createdBefore} - onChange={(ev) => setCreatedBefore(ev.target.value)} + onChange={(ev) => + changeListFilter({ + field: 'createdBefore', + value: ev.target.value, + }) + } autoComplete="off" min={FIRST_EVENT_TIMESTAMP} max={new Date().toISOString().split('.')[0]} @@ -256,7 +277,9 @@ export const ModEventList = ( name="sortDirection" className="flex items-center" checked={oldestFirst} - onChange={() => setOldestFirst((current) => !current)} + onChange={() => + changeListFilter({ field: 'oldestFirst', value: !oldestFirst }) + } label="Show oldest events first (default: newest first)" />
@@ -269,7 +292,11 @@ export const ModEventList = ( No moderation events found. {!!types.length && (

- setTypes([])}> + resetListFilters()} + > Clear all filters {' '} to see all events diff --git a/components/mod-event/useModEventList.tsx b/components/mod-event/useModEventList.tsx index 837a90ac..a086effd 100644 --- a/components/mod-event/useModEventList.tsx +++ b/components/mod-event/useModEventList.tsx @@ -1,6 +1,6 @@ import { useInfiniteQuery } from '@tanstack/react-query' import client from '@/lib/client' -import { useContext, useEffect, useState } from 'react' +import { useContext, useEffect, useReducer, useState } from 'react' import { AuthContext } from '@/shell/AuthContext' import { ComAtprotoAdminQueryModerationEvents } from '@atproto/api' import { MOD_EVENT_TITLES } from './constants' @@ -18,58 +18,95 @@ type CommentFilter = { export const FIRST_EVENT_TIMESTAMP = '2022-11-01T00:00' const allTypes = Object.keys(MOD_EVENT_TITLES) +const initialListState = { + types: allTypes, + includeAllUserRecords: false, + commentFilter: { enabled: false, keyword: '' }, + createdBy: undefined, + subject: undefined, + oldestFirst: false, + createdBefore: new Date().toISOString().split('.')[0], + createdAfter: FIRST_EVENT_TIMESTAMP, +} + +// The 2 fields need overriding because in the initialState, they are set as undefined so the alternative string type is not accepted without override +type EventListState = Omit & { + subject?: string + createdBy?: string +} + +type EventListFilterPayload = + | { field: 'types'; value: string[] } + | { field: 'includeAllUserRecords'; value: boolean } + | { field: 'commentFilter'; value: CommentFilter } + | { field: 'createdBy'; value: string | undefined } + | { field: 'subject'; value: string | undefined } + | { field: 'oldestFirst'; value: boolean } + | { field: 'createdBefore'; value: string } + | { field: 'createdAfter'; value: string } + +type EventListAction = + | { + type: 'SET_FILTER' + payload: EventListFilterPayload + } + | { + type: 'RESET' + } + +const eventListReducer = (state: EventListState, action: EventListAction) => { + switch (action.type) { + case 'SET_FILTER': + return { ...state, [action.payload.field]: action.payload.value } + case 'RESET': + return initialListState + default: + return state + } +} export const useModEventList = ( props: { subject?: string; createdBy?: string } & ModEventListQueryOptions, ) => { const { isLoggedIn } = useContext(AuthContext) - const [createdAfter, setCreatedAfter] = useState( - FIRST_EVENT_TIMESTAMP, - ) - const [createdBefore, setCreatedBefore] = useState( - new Date().toISOString().split('.')[0], - ) - const [subject, setSubject] = useState(props.subject) - const [createdBy, setCreatedBy] = useState( - props.createdBy, - ) - const [types, setTypes] = useState(allTypes) - const [oldestFirst, setOldestFirst] = useState(false) - const [commentFilter, setCommentFilter] = useState({ - enabled: false, - keyword: '', - }) - const [includeAllUserRecords, setIncludeAllUserRecords] = - useState(false) + const [listState, dispatch] = useReducer(eventListReducer, initialListState) + + const setCommentFilter = (value: CommentFilter) => { + dispatch({ type: 'SET_FILTER', payload: { field: 'commentFilter', value } }) + } useEffect(() => { - if (props.subject !== subject) { - setSubject(props.subject) + if (props.subject !== listState.subject) { + dispatch({ + type: 'SET_FILTER', + payload: { field: 'subject', value: props.subject }, + }) } }, [props.subject]) useEffect(() => { - if (props.createdBy !== createdBy) { - setCreatedBy(props.createdBy) + if (props.createdBy !== listState.createdBy) { + dispatch({ + type: 'SET_FILTER', + payload: { field: 'createdBy', value: props.createdBy }, + }) } }, [props.createdBy]) const results = useInfiniteQuery({ enabled: isLoggedIn, - queryKey: [ - 'modEventList', - { - createdBy, - subject, + queryKey: ['modEventList', { listState }], + queryFn: async ({ pageParam }) => { + const { types, includeAllUserRecords, commentFilter, + createdBy, + subject, oldestFirst, - createdAfter, createdBefore, - }, - ], - queryFn: async ({ pageParam }) => { + createdAfter, + } = listState const queryParams: ComAtprotoAdminQueryModerationEvents.QueryParams = { cursor: pageParam, includeAllUserRecords, @@ -93,7 +130,7 @@ export const useModEventList = ( const filterTypes = types.filter(Boolean) if (filterTypes.length < allTypes.length && filterTypes.length > 0) { - queryParams.types = allTypes + queryParams.types = filterTypes } if (oldestFirst) { @@ -115,47 +152,41 @@ export const useModEventList = ( }) const hasFilter = - (types.length > 0 && - types.length !== Object.keys(MOD_EVENT_TITLES).length) || - includeAllUserRecords || - commentFilter.enabled || - createdBy || - subject || - oldestFirst + (listState.types.length > 0 && + listState.types.length !== allTypes.length) || + listState.includeAllUserRecords || + listState.commentFilter.enabled || + listState.createdBy || + listState.subject || + listState.oldestFirst return { - types, - setTypes, - includeAllUserRecords, - setIncludeAllUserRecords, + // Data from react-query modEvents: results.data?.pages.map((page) => page.events).flat() || [], fetchMoreModEvents: results.fetchNextPage, hasMoreModEvents: results.hasNextPage, refetchModEvents: results.refetch, isInitialLoadingModEvents: results.isInitialLoading, - hasFilter, - commentFilter, + + // Helper functions to mutate state toggleCommentFilter: () => { - setCommentFilter((prev) => { - if (prev.enabled) { - return { enabled: false, keyword: '' } - } - return { enabled: true, keyword: '' } - }) + if (listState.commentFilter.enabled) { + return setCommentFilter({ enabled: false, keyword: '' }) + } + return setCommentFilter({ enabled: true, keyword: '' }) }, setCommentFilterKeyword: (keyword: string) => { setCommentFilter({ enabled: true, keyword }) }, - createdBy, - setCreatedBy, - subject, - setSubject, - setOldestFirst, - oldestFirst, - createdBefore, - setCreatedBefore, - createdAfter, - setCreatedAfter, + changeListFilter: (payload: EventListFilterPayload) => + dispatch({ type: 'SET_FILTER', payload: payload }), + resetListFilters: () => dispatch({ type: 'RESET' }), + + // State data + ...listState, + + // Derived data from state + hasFilter, } } From 116e6f9e7b8c7689ec669f543298609a55849e90 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Fri, 2 Feb 2024 15:14:46 +0100 Subject: [PATCH 3/6] :lipstick: Fix spacing around the filter panel --- app/actions/ModActionPanel/QuickAction.tsx | 2 +- components/common/FullScreenActionPanel.tsx | 2 +- components/mod-event/EventList.tsx | 30 ++++++++++----------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/app/actions/ModActionPanel/QuickAction.tsx b/app/actions/ModActionPanel/QuickAction.tsx index 49cb12c9..053bec2e 100644 --- a/app/actions/ModActionPanel/QuickAction.tsx +++ b/app/actions/ModActionPanel/QuickAction.tsx @@ -427,7 +427,7 @@ function Form( } } `} -

+
- +