From 314efe1ba8e635574a9079f5ab9e59aa1fded1da Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Fri, 27 Dec 2024 19:40:50 +0000 Subject: [PATCH 01/10] :construction: WIP --- app/actions/ModActionPanel/QuickAction.tsx | 2 +- components/reports/QueueFilter/Panel.tsx | 8 +- .../reports/QueueFilter/SubjectType.tsx | 55 ++--- components/reports/QueueFilter/Tag.tsx | 193 ++++++++++++++++++ components/reports/useQueueFilter.tsx | 8 + 5 files changed, 224 insertions(+), 42 deletions(-) create mode 100644 components/reports/QueueFilter/Tag.tsx diff --git a/app/actions/ModActionPanel/QuickAction.tsx b/app/actions/ModActionPanel/QuickAction.tsx index 4597401..2a3575e 100644 --- a/app/actions/ModActionPanel/QuickAction.tsx +++ b/app/actions/ModActionPanel/QuickAction.tsx @@ -636,7 +636,7 @@ function Form(
- {subjectStatus.tags.map((tag) => { + {subjectStatus.tags.sort().map((tag) => { return })} diff --git a/components/reports/QueueFilter/Panel.tsx b/components/reports/QueueFilter/Panel.tsx index 52658ad..1698e76 100644 --- a/components/reports/QueueFilter/Panel.tsx +++ b/components/reports/QueueFilter/Panel.tsx @@ -8,6 +8,7 @@ import { ToolsOzoneModerationQueryStatuses } from '@atproto/api' import { getLanguageFlag } from 'components/tags/SubjectTag' import { getCollectionName } from '../helpers/subject' import { classNames } from '@/lib/util' +import { QueueFilterTag, QueueFilterTags } from './Tag' // Takes all the queue filters manageable in the panel and displays a summary of selections made const FilterSummary = ({ @@ -117,12 +118,13 @@ export const QueueFilterPanel = () => { leaveFrom="transform scale-100 opacity-100" leaveTo="transform scale-95 opacity-0" > - -
+ +
-
+ +
diff --git a/components/reports/QueueFilter/SubjectType.tsx b/components/reports/QueueFilter/SubjectType.tsx index f9ae891..6709211 100644 --- a/components/reports/QueueFilter/SubjectType.tsx +++ b/components/reports/QueueFilter/SubjectType.tsx @@ -63,44 +63,23 @@ export const QueueFilterSubjectType = () => { Record Collection - {Object.values(CollectionId).map((collectionId) => { - const isSelected = selectedCollections.includes(collectionId) - return ( - { - toggleCollection(collectionId) - }} - /> - ) - })} -
-
-

- Record Embed -

- {[...allEmbedTypes, 'noEmbed'].map((embedType) => { - const isNoEmbed = embedType === 'noEmbed' - const isSelected = isNoEmbed - ? allEmbedTypes.every((et) => - selectedExcludeEmbedTypes.includes(et), - ) - : selectedIncludeEmbedTypes.includes(embedType) - return ( - { - toggleEmbedType(embedType) - }} - /> - ) - })} +
+ {Object.values(CollectionId).map((collectionId) => { + const isSelected = selectedCollections.includes(collectionId) + return ( + { + toggleCollection(collectionId) + }} + /> + ) + })} +
)} diff --git a/components/reports/QueueFilter/Tag.tsx b/components/reports/QueueFilter/Tag.tsx new file mode 100644 index 0000000..6412d35 --- /dev/null +++ b/components/reports/QueueFilter/Tag.tsx @@ -0,0 +1,193 @@ +import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/24/solid' +import { Combobox, Transition } from '@headlessui/react' +import { Fragment, useState } from 'react' +import { isNonNullable } from '@/lib/util' +import { useQueueFilter } from '../useQueueFilter' +import { LabelChip } from '@/common/labels' +import { ActionButton } from '@/common/buttons' + +const availableTagOptions = { + report: { + text: 'Report', + options: [ + { text: 'Spam', value: 'report:spam' }, + { text: 'Violation', value: 'report:violation' }, + { text: 'Misleading', value: 'report:misleading' }, + ], + }, + lang: { + text: 'Language', + options: [ + { text: 'English', value: 'lang:en' }, + { text: 'Portuguese', value: 'lang:pt' }, + { text: 'Spanish', value: 'lang:es' }, + { text: 'French', value: 'lang:fr' }, + ], + }, + embed: { + text: 'Embed', + options: [ + { text: 'Image', value: 'embed:image' }, + { text: 'Video', value: 'embed:video' }, + { text: 'Link/External', value: 'embed:external' }, + ], + }, +} + +export const QueueFilterTags = () => { + const { addTags, queueFilters } = useQueueFilter() + // @TODO: This should move to dynamic indexing + const currentTags = queueFilters.tags ?? [''] + console.log(queueFilters.tags) + + return ( +
+

Tag Filters

+ {currentTags.map((tags, i) => { + const fragments = tags.split('&&').filter(Boolean) + return ( + <> + {i > 0 &&
OR
} +
+ {fragments.map((tag, i) => { + return ( + <> + {tag} + {i + 1 < fragments.length && ( + AND + )} + + ) + })} + {i + 1 === currentTags.length && fragments.length > 0 && ( + + )} +
+ + ) + })} + addTags(currentTags.length - 1, selections)} + /> +
+ ) +} + +export const QueueFilterTag = ({ + selected, + onSelect, +}: { + selected: string + onSelect: (tags: string[]) => void +}) => { + const [query, setQuery] = useState('') + const filteredTagOptions = Object.values(availableTagOptions) + .map((group) => { + const options = group.options.filter((option) => + option.text.toLowerCase().includes(query.toLowerCase()), + ) + + if (options.length) { + return { ...group, options } + } + + return null + }) + .filter(isNonNullable) + + return ( + { + onSelect(selections) + }} + name="template" + > +
+
+ setQuery(event.target.value)} + placeholder="Type keyword or click the arrows on the right to see all templates" + /> + + +
+ setQuery('')} + > + + {!filteredTagOptions.length && ( +

+ No result for {`"${query}"`} +

+ )} + {filteredTagOptions.map((group) => { + return ( +
+

{group.text}

+ {group.options.map((option) => { + return ( + + `relative cursor-default select-none py-2 pl-10 pr-4 ${ + active + ? 'bg-gray-100 dark:bg-slate-600 text-gray-900 dark:text-gray-200' + : 'text-gray-900 dark:text-gray-200' + }` + } + value={option.value} + > + {({ selected, active }) => ( + <> + {selected ? ( + + + ) : null} +
+ + {option.text} + +
+ + )} +
+ ) + })} +
+ ) + })} +
+
+
+
+ ) +} diff --git a/components/reports/useQueueFilter.tsx b/components/reports/useQueueFilter.tsx index 04ef625..e54e10f 100644 --- a/components/reports/useQueueFilter.tsx +++ b/components/reports/useQueueFilter.tsx @@ -153,6 +153,13 @@ export const useQueueFilter = () => { }) } + const addTags = (index: number, tags: string[]) => { + console.log({ index, tags }) + const newTags = queueFilters.tags ?? [] + newTags[index] = tags.filter(Boolean).join('&&') + updateFilters({ tags: newTags }) + } + const clearLanguages = () => { const newTags = queueFilters.tags?.filter((tag) => !tag.startsWith('lang:')) const newExcludeTags = queueFilters.excludeTags?.filter( @@ -174,5 +181,6 @@ export const useQueueFilter = () => { toggleEmbedType, clearLanguages, toggleLanguage, + addTags, } } From 4c095873a2f904ea9131b90ad1cfb7d61c94b774 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Mon, 30 Dec 2024 12:01:27 +0000 Subject: [PATCH 02/10] :bug: Ignore keybinding for takedown unless user has permission --- app/actions/ModActionPanel/QuickAction.tsx | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/app/actions/ModActionPanel/QuickAction.tsx b/app/actions/ModActionPanel/QuickAction.tsx index 2a3575e..9b743d3 100644 --- a/app/actions/ModActionPanel/QuickAction.tsx +++ b/app/actions/ModActionPanel/QuickAction.tsx @@ -32,7 +32,7 @@ import { ChevronUpIcon, } from '@heroicons/react/24/outline' import { LabelSelector } from '@/common/labels/Selector' -import { pluralize, takesKeyboardEvt } from '@/lib/util' +import { takesKeyboardEvt } from '@/lib/util' import { Loading } from '@/common/Loader' import { ActionDurationSelector } from '@/reports/ModerationForm/ActionDurationSelector' import { MOD_EVENTS } from '@/mod-event/constants' @@ -200,6 +200,7 @@ function Form( const shouldShowDurationInHoursField = isTakedownEvent || isMuteEvent || isMuteReporterEvent const canManageChat = usePermission('canManageChat') + const canTakedown = usePermission('canTakedown') // navigate to next or prev report const navigateQueue = (delta: 1 | -1) => { @@ -491,7 +492,13 @@ function Form( submitForm() } useKeyPressEvent('c', safeKeyHandler(onCancel)) - useKeyPressEvent('s', safeKeyHandler(submitForm)) + useKeyPressEvent( + 's', + safeKeyHandler((e) => { + e.stopImmediatePropagation() + submitForm() + }), + ) useKeyPressEvent('n', safeKeyHandler(submitAndGoNext)) useKeyPressEvent( 'a', @@ -513,9 +520,11 @@ function Form( ) useKeyPressEvent( 't', - safeKeyHandler(() => { - setModEventType(MOD_EVENTS.TAKEDOWN) - }), + canTakedown + ? safeKeyHandler(() => { + setModEventType(MOD_EVENTS.TAKEDOWN) + }) + : undefined, ) return ( From 055dcc86056df173549d68d7ecfc6d6da2b1b62a Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Mon, 30 Dec 2024 12:04:14 +0000 Subject: [PATCH 03/10] :sparkles: Remove cmd palette shortcuts --- components/shell/CommandPalette/actions.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/components/shell/CommandPalette/actions.ts b/components/shell/CommandPalette/actions.ts index 36bed9c..2242df5 100644 --- a/components/shell/CommandPalette/actions.ts +++ b/components/shell/CommandPalette/actions.ts @@ -13,7 +13,6 @@ export const getStaticActions = ({ { id: 'quick-action-modal', name: 'Open Quick Action Panel', - shortcut: ['q'], keywords: 'quick,action,panel', perform: () => { router.push('/reports?quickOpen=true') @@ -22,7 +21,6 @@ export const getStaticActions = ({ { id: 'workspace-modal', name: 'Open Workspace', - shortcut: ['w'], keywords: 'workspace,panel', perform: () => { const newParams = new URLSearchParams(searchParams) @@ -33,7 +31,6 @@ export const getStaticActions = ({ { id: 'unresolved-queue', name: 'Open Unresolved Queue', - shortcut: ['u'], keywords: 'unresolved,queue', perform: () => router.push( @@ -43,7 +40,6 @@ export const getStaticActions = ({ { id: 'resolved-queue', name: 'Open Resolved Queue', - shortcut: ['r'], keywords: 'resolved,queue', perform: () => router.push( @@ -53,7 +49,6 @@ export const getStaticActions = ({ { id: 'escalated-queue', name: 'Open Escalated Queue', - shortcut: ['e'], keywords: 'escalated,queue', perform: () => router.push( @@ -63,14 +58,12 @@ export const getStaticActions = ({ { id: 'all-queue', name: 'Open Moderation Queue', - shortcut: ['a'], keywords: 'all,queue', perform: () => router.push('/reports'), }, { id: 'appeal-queue', name: 'Open Appeal Queue', - shortcut: ['e'], keywords: 'appealed,queue', perform: () => router.push( @@ -80,14 +73,12 @@ export const getStaticActions = ({ { id: 'filter-macros', name: 'Manage Filter Macros', - shortcut: ['f'], keywords: 'filter,macros', perform: () => router.push('/events/filters/macros'), }, { id: 'view-sets', name: 'See All Sets', - shortcut: ['s'], keywords: 'sets,settings', perform: () => { router.push('/configure?tab=sets') From 5ddeacc6897c198619f21110866736f62ed13055 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Mon, 30 Dec 2024 12:28:40 +0000 Subject: [PATCH 04/10] :lipstick: Fix styling for tetris page for dark mode --- app/surprise-me/page.tsx | 38 +------------------ components/entertainment/tetris.tsx | 2 +- styles/globals.css | 57 +++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 37 deletions(-) diff --git a/app/surprise-me/page.tsx b/app/surprise-me/page.tsx index 958a040..4c546c1 100644 --- a/app/surprise-me/page.tsx +++ b/app/surprise-me/page.tsx @@ -29,7 +29,7 @@ const Timer = () => { }, 1000) return ( -
+

{getDuration(seconds)}

) @@ -44,41 +44,7 @@ export default function SurpriseMePage() { <> {/* This is valid jsx but because of a known bug, typescript is confused */} {/* @ts-ignore:next-line */} - + diff --git a/components/entertainment/tetris.tsx b/components/entertainment/tetris.tsx index 253b079..c78555e 100644 --- a/components/entertainment/tetris.tsx +++ b/components/entertainment/tetris.tsx @@ -27,7 +27,7 @@ export default function TetrisGame () { state, controller, }) => ( -
+

Points: {points}

diff --git a/styles/globals.css b/styles/globals.css index 1a0afa6..9d8e6ef 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -5,4 +5,61 @@ .wmde-markdown-var[data-color-mode*='dark'] { --color-canvas-default: theme(colors.slate.800) !important; --color-border-default: theme(colors.teal.500) !important; +} + +.game-block { + @apply m-0 p-0 w-6 h-6 border border-gray-300; +} +.piece-i { + @apply bg-pink-400; +} +.piece-j { + @apply bg-orange-300; +} +.piece-l { + @apply bg-yellow-200; +} +.piece-o { + @apply bg-yellow-500; +} +.piece-s { + @apply bg-gray-700; +} +.piece-t { + @apply bg-purple-400; +} +.piece-z { + @apply bg-red-400; +} +.piece-preview { + @apply bg-gray-200; +} + + +.dark .game-block { + @apply border-gray-600; +} +.dark .piece-i { + @apply bg-pink-700; +} +.dark .piece-j { + @apply bg-orange-500; +} +.dark .piece-l { + @apply bg-yellow-400; +} +.dark .piece-o { + @apply bg-yellow-700; +} +.dark .piece-s { + @apply bg-gray-800; +} +.dark .piece-t { + @apply bg-purple-600; +} +.dark .piece-z { + @apply bg-red-600; +} +.dark .piece-preview { + @apply bg-gray-700; } \ No newline at end of file From 7b2ba1fa425af81e131b70f006663c17e9e64a21 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Mon, 30 Dec 2024 12:37:11 +0000 Subject: [PATCH 05/10] :lipstick: Fix tag list overflow in profile page --- components/repositories/AccountView.tsx | 2 +- components/shell/CommandPalette/ResultItem.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/repositories/AccountView.tsx b/components/repositories/AccountView.tsx index 16dcd71..eec6c9b 100644 --- a/components/repositories/AccountView.tsx +++ b/components/repositories/AccountView.tsx @@ -640,7 +640,7 @@ function Details({ - + {!tags.length && } {tags.map((tag) => ( diff --git a/components/shell/CommandPalette/ResultItem.tsx b/components/shell/CommandPalette/ResultItem.tsx index c12ad21..1182007 100644 --- a/components/shell/CommandPalette/ResultItem.tsx +++ b/components/shell/CommandPalette/ResultItem.tsx @@ -28,7 +28,7 @@ const ResultItem = ( active ? 'bg-blue-400 rounded-lg text-gray-100 ' : 'transparent text-gray-500' - } 'rounded-lg px-4 py-2 flex items-center cursor-pointer justify-between `} + } rounded-lg px-4 py-3 flex items-center cursor-pointer justify-between `} >
{action.icon && action.icon} From 343ef37299df4e07d8664156381f85eda691cc84 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Mon, 30 Dec 2024 12:42:50 +0000 Subject: [PATCH 06/10] :lipstick: De-emphasize bluesky branding --- components/common/SetupModal.tsx | 4 ++-- components/shell/MobileMenu.tsx | 2 +- components/shell/Shell.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/components/common/SetupModal.tsx b/components/common/SetupModal.tsx index e043cde..ddfe3b9 100644 --- a/components/common/SetupModal.tsx +++ b/components/common/SetupModal.tsx @@ -27,12 +27,12 @@ export function SetupModal({ className="mx-auto h-20 w-auto" title="Icon from Flaticon: https://www.flaticon.com/free-icons/lifeguard-tower" src="/img/logo-colorful.png" - alt="Ozone - Bluesky Admin" + alt="Ozone - ATProto Moderation Service" width={200} height={200} />

- Bluesky Admin Tools + Ozone Moderation Service

{title && (

diff --git a/components/shell/MobileMenu.tsx b/components/shell/MobileMenu.tsx index 1d0d386..5c7b9b2 100644 --- a/components/shell/MobileMenu.tsx +++ b/components/shell/MobileMenu.tsx @@ -109,7 +109,7 @@ export function MobileMenu({ toggleTheme }: { toggleTheme: () => void }) { title="Icon from Flaticon: https://www.flaticon.com/free-icons/lifeguard-tower" className="h-8 w-auto" src="/img/logo-white.png" - alt="Ozone - Bluesky Admin" + alt="Ozone - ATProto Moderation Service" />

diff --git a/components/shell/Shell.tsx b/components/shell/Shell.tsx index 752313d..f280604 100644 --- a/components/shell/Shell.tsx +++ b/components/shell/Shell.tsx @@ -28,7 +28,7 @@ export function Shell({ children }: React.PropsWithChildren) { height={100} className="h-8 w-auto" src="/img/logo-white.png" - alt="Ozone - Bluesky Admin" + alt="Ozone - ATProto Moderation Service" title="Icon from Flaticon: https://www.flaticon.com/free-icons/lifeguard-tower" />
From efa025a2657cf3d9e1f14a0d95e21ac81a17be74 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Mon, 30 Dec 2024 22:11:44 +0000 Subject: [PATCH 07/10] :sparkles: Add tag filtering with exclusion --- app/actions/ModActionPanel/QuickAction.tsx | 2 +- components/reports/QueueFilter/Panel.tsx | 4 +- .../reports/QueueFilter/SubjectType.tsx | 36 +- components/reports/QueueFilter/Tag.tsx | 331 ++++++++++-------- components/reports/useQueueFilter.tsx | 41 ++- components/tags/SubjectTag.tsx | 8 +- package.json | 1 + yarn.lock | 5 + 8 files changed, 239 insertions(+), 189 deletions(-) diff --git a/app/actions/ModActionPanel/QuickAction.tsx b/app/actions/ModActionPanel/QuickAction.tsx index 9b743d3..f0abb8a 100644 --- a/app/actions/ModActionPanel/QuickAction.tsx +++ b/app/actions/ModActionPanel/QuickAction.tsx @@ -644,7 +644,7 @@ function Form( {!!subjectStatus?.tags?.length && (
- + {subjectStatus.tags.sort().map((tag) => { return })} diff --git a/components/reports/QueueFilter/Panel.tsx b/components/reports/QueueFilter/Panel.tsx index 1698e76..d8cd8e6 100644 --- a/components/reports/QueueFilter/Panel.tsx +++ b/components/reports/QueueFilter/Panel.tsx @@ -8,7 +8,7 @@ import { ToolsOzoneModerationQueryStatuses } from '@atproto/api' import { getLanguageFlag } from 'components/tags/SubjectTag' import { getCollectionName } from '../helpers/subject' import { classNames } from '@/lib/util' -import { QueueFilterTag, QueueFilterTags } from './Tag' +import { QueueFilterTags } from './Tag' // Takes all the queue filters manageable in the panel and displays a summary of selections made const FilterSummary = ({ @@ -18,7 +18,7 @@ const FilterSummary = ({ }) => { const { tags, excludeTags, collections, subjectType } = queueFilters if ( - !tags?.length && + !tags?.filter(Boolean).length && !excludeTags?.length && !collections?.length && !subjectType diff --git a/components/reports/QueueFilter/SubjectType.tsx b/components/reports/QueueFilter/SubjectType.tsx index 6709211..1786506 100644 --- a/components/reports/QueueFilter/SubjectType.tsx +++ b/components/reports/QueueFilter/SubjectType.tsx @@ -1,32 +1,40 @@ +import { XCircleIcon } from '@heroicons/react/24/solid' import { ButtonGroup } from '@/common/buttons' -import { - CollectionId, - EmbedTypes, - getCollectionName, - getEmbedTypeName, -} from '../helpers/subject' +import { CollectionId, getCollectionName } from '../helpers/subject' import { Checkbox } from '@/common/forms' import { useQueueFilter } from '../useQueueFilter' export const QueueFilterSubjectType = () => { - const { queueFilters, toggleCollection, toggleSubjectType, toggleEmbedType } = - useQueueFilter() - const allEmbedTypes = Object.values(EmbedTypes) + const { + queueFilters, + toggleCollection, + toggleSubjectType, + clearSubjectType, + } = useQueueFilter() const selectedCollections = queueFilters.collections || [] + const hasSubjectTypeFilter = + !!queueFilters.subjectType || !!selectedCollections.length const selectedIncludeEmbedTypes: string[] = queueFilters.tags?.filter((tag) => { return tag.startsWith('embed:') }) || [] - const selectedExcludeEmbedTypes: string[] = - queueFilters.excludeTags?.filter((tag) => { - return tag.startsWith('embed:') - }) || [] return (

- Subject Type Filters +

{ + const { embed, ...rest } = availableTagOptions + return Object.values(subjectType === 'account' ? rest : availableTagOptions) +} + +const selectClassNames = { + tagItemIconContainer: + 'flex items-center px-1 cursor-pointer rounded-r-sm hover:bg-red-200 hover:text-red-600 dark:text-slate-900', + menuButton: ({ isDisabled }: { isDisabled?: boolean } = {}) => + classNames( + isDisabled ? 'bg-gray-200' : 'bg-white hover:border-gray-400 focus:ring', + 'flex text-sm text-gray-500 border border-gray-300 rounded shadow-sm transition-all duration-300 focus:outline-none dark:bg-slate-700 dark:text-gray-100', + ), + menu: 'absolute z-10 w-full bg-white shadow-lg border rounded py-1 mt-1.5 text-sm text-gray-700 dark:bg-slate-700 dark:text-gray-100', + searchBox: + 'w-full py-2 pl-8 text-sm text-gray-500 bg-gray-100 border border-gray-200 rounded focus:border-gray-200 focus:ring-0 focus:outline-none dark:bg-slate-800 dark:text-gray-100', + listGroupLabel: + 'pr-2 py-2 cursor-default select-none truncate text-gray-700 dark:text-gray-100', + listItem: ({ isSelected }: { isSelected?: boolean } = {}) => { + const baseClass = + 'block transition duration-200 px-2 py-2 cursor-pointer select-none truncate rounded dark:hover:bg-slate-800 dark:hover:text-gray-200' + const selectedClass = isSelected + ? `text-white` + : `text-gray-500 dark:text-gray-300` + + return classNames(baseClass, selectedClass) }, + tagItemText: `text-gray-600 text-xs truncate cursor-default select-none`, } export const QueueFilterTags = () => { - const { addTags, queueFilters } = useQueueFilter() - // @TODO: This should move to dynamic indexing + const { addTags, updateTagExclusions, clearTags, queueFilters } = + useQueueFilter() const currentTags = queueFilters.tags ?? [''] - console.log(queueFilters.tags) + const hasTagFilters = currentTags.filter(Boolean).length > 0 + const lastFragmentHasTags = !!currentTags[currentTags.length - 1].length + const allExcludedTags = queueFilters.excludeTags?.join() + const allTagFilters = currentTags.join() + + const allTagOptions = getTagOptions(queueFilters.subjectType) + // If a tag is already set to be excluded, don't let that tag be set as a filter + const tagOptions = allExcludedTags?.length + ? allTagOptions.map((group) => { + return { + ...group, + options: group.options.filter( + (option) => !allExcludedTags.includes(option.value), + ), + } + }) + : allTagOptions + + // If a tag is already set in the filters, don't let that tag be set as an exclusion + const exclusionTagOptions = allTagFilters.length + ? allTagOptions.map((group) => { + return { + ...group, + options: group.options.filter( + (option) => !allTagFilters.includes(option.value), + ), + } + }) + : allTagOptions + + const currentTagExclusions = queueFilters.excludeTags ?? [] + const hasTagExclusions = currentTagExclusions.length > 0 return ( -
-

Tag Filters

+
+

+ +

{currentTags.map((tags, i) => { const fragments = tags.split('&&').filter(Boolean) return ( <> - {i > 0 &&
OR
} -
- {fragments.map((tag, i) => { - return ( - <> - {tag} - {i + 1 < fragments.length && ( - AND - )} - - ) - })} - {i + 1 === currentTags.length && fragments.length > 0 && ( + {i > 0 && ( +
- )} -
+
+ )} + ({ label: tag, value: tag }))} + onChange={(selections) => + updateTagExclusions( + Array.isArray(selections) ? selections.map((s) => s.value) : [], + ) + } + options={exclusionTagOptions.map((group) => ({ + label: group.text, + options: group.options.map((option) => ({ + label: option.text, + value: option.value, + isSelected: currentTagExclusions.includes(option.value), + })), + }))} />
) } - -export const QueueFilterTag = ({ - selected, - onSelect, -}: { - selected: string - onSelect: (tags: string[]) => void -}) => { - const [query, setQuery] = useState('') - const filteredTagOptions = Object.values(availableTagOptions) - .map((group) => { - const options = group.options.filter((option) => - option.text.toLowerCase().includes(query.toLowerCase()), - ) - - if (options.length) { - return { ...group, options } - } - - return null - }) - .filter(isNonNullable) - - return ( - { - onSelect(selections) - }} - name="template" - > -
-
- setQuery(event.target.value)} - placeholder="Type keyword or click the arrows on the right to see all templates" - /> - - -
- setQuery('')} - > - - {!filteredTagOptions.length && ( -

- No result for {`"${query}"`} -

- )} - {filteredTagOptions.map((group) => { - return ( -
-

{group.text}

- {group.options.map((option) => { - return ( - - `relative cursor-default select-none py-2 pl-10 pr-4 ${ - active - ? 'bg-gray-100 dark:bg-slate-600 text-gray-900 dark:text-gray-200' - : 'text-gray-900 dark:text-gray-200' - }` - } - value={option.value} - > - {({ selected, active }) => ( - <> - {selected ? ( - - - ) : null} -
- - {option.text} - -
- - )} -
- ) - })} -
- ) - })} -
-
-
-
- ) -} diff --git a/components/reports/useQueueFilter.tsx b/components/reports/useQueueFilter.tsx index e54e10f..7478993 100644 --- a/components/reports/useQueueFilter.tsx +++ b/components/reports/useQueueFilter.tsx @@ -86,6 +86,13 @@ export const useQueueFilter = () => { updateFilters(newParams) } + const clearSubjectType = () => { + updateFilters({ + subjectType: undefined, + collections: undefined, + }) + } + const toggleEmbedType = (embedType: string) => { const allEmbedTypes = Object.values(EmbedTypes) @@ -136,28 +143,24 @@ export const useQueueFilter = () => { }) } - const toggleLanguage = (section: 'include' | 'exclude', newLang: string) => { - const filterKey = section === 'include' ? 'tags' : 'excludeTags' - const currentTags = - section === 'include' ? queueFilters.tags : queueFilters.excludeTags + const addTags = (index: number, tags: string[]) => { + const newTags = queueFilters.tags ?? [] - const newTags = new Set(currentTags ?? []) - if (newTags.has(`lang:${newLang}`)) { - newTags.delete(`lang:${newLang}`) - } else { - newTags.add(`lang:${newLang}`) + if (!tags.length) { + newTags.splice(index, 1) + return updateFilters({ tags: newTags }) } - updateFilters({ - [filterKey]: newTags.size > 0 ? Array.from(newTags) : undefined, - }) + newTags[index] = tags.join('&&') + updateFilters({ tags: newTags }) } - const addTags = (index: number, tags: string[]) => { - console.log({ index, tags }) - const newTags = queueFilters.tags ?? [] - newTags[index] = tags.filter(Boolean).join('&&') - updateFilters({ tags: newTags }) + const updateTagExclusions = (excludeTags: string[]) => { + updateFilters({ excludeTags: excludeTags.length ? excludeTags : undefined }) + } + + const clearTags = () => { + updateFilters({ tags: [] }) } const clearLanguages = () => { @@ -177,10 +180,12 @@ export const useQueueFilter = () => { updateFilters, toggleCollection, resetFilters, + clearSubjectType, toggleSubjectType, toggleEmbedType, clearLanguages, - toggleLanguage, + clearTags, + updateTagExclusions, addTags, } } diff --git a/components/tags/SubjectTag.tsx b/components/tags/SubjectTag.tsx index e3b0cf9..604dfd9 100644 --- a/components/tags/SubjectTag.tsx +++ b/components/tags/SubjectTag.tsx @@ -1,5 +1,6 @@ import { LabelChip } from '@/common/labels' import { LANGUAGES_MAP_CODE2 } from '@/lib/locale/languages' +import { ComponentProps } from 'react' export const getLanguageFlag = (langCode: string) => { if (!langCode) return undefined @@ -8,7 +9,10 @@ export const getLanguageFlag = (langCode: string) => { return langDetails?.flag } -export const SubjectTag = ({ tag }: { tag: string }) => { +export const SubjectTag = ({ + tag, + ...rest +}: { tag: string } & ComponentProps) => { if (tag.startsWith('lang:')) { const langCode = tag.split(':')[1]?.toLowerCase() const langDetails = LANGUAGES_MAP_CODE2[langCode] @@ -21,5 +25,5 @@ export const SubjectTag = ({ tag }: { tag: string }) => { ) } } - return {tag} + return {tag} } diff --git a/package.json b/package.json index c593da3..7b8d3d9 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "react-dom": "18.2.0", "react-dropzone": "^14.3.5", "react-json-view": "1.21.3", + "react-tailwindcss-select": "^1.8.5", "react-tetris": "^0.3.0", "react-toastify": "^9.1.1", "react-use": "^17.4.0", diff --git a/yarn.lock b/yarn.lock index af4ccfd..b04f730 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4497,6 +4497,11 @@ react-markdown@~8.0.0: unist-util-visit "^4.0.0" vfile "^5.0.0" +react-tailwindcss-select@^1.8.5: + version "1.8.5" + resolved "https://registry.yarnpkg.com/react-tailwindcss-select/-/react-tailwindcss-select-1.8.5.tgz#adf752c7e54889c38fd76bd1aa5cf8cf6d2b9788" + integrity sha512-x29IrLiqBT5FnkC9oFQReOr05tEOZHtDtZdha84nlSWcj3qD67yonKFHXIf69yc8ElFlKUxCEv0zCllN8jHBFA== + react-tetris@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/react-tetris/-/react-tetris-0.3.0.tgz#b846e1dff9af37880a54877783aedddc2258bed5" From 8a2bce56b4c588749a8fd7371a464f391f4bf80d Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Tue, 31 Dec 2024 15:42:12 +0000 Subject: [PATCH 08/10] :broom: Cleanup --- components/reports/QueueFilter/Language.tsx | 103 -------------------- components/reports/QueueFilter/Panel.tsx | 1 - components/reports/QueueFilter/Tag.tsx | 4 +- components/reports/useQueueFilter.tsx | 63 +----------- 4 files changed, 6 insertions(+), 165 deletions(-) delete mode 100644 components/reports/QueueFilter/Language.tsx diff --git a/components/reports/QueueFilter/Language.tsx b/components/reports/QueueFilter/Language.tsx deleted file mode 100644 index b171e3a..0000000 --- a/components/reports/QueueFilter/Language.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { getLanguageName } from '@/lib/locale/helpers' -import { CheckIcon } from '@heroicons/react/20/solid' -import { ActionButton } from '@/common/buttons' -import { availableLanguageCodes } from '@/common/LanguagePicker' -import { useQueueFilter } from '../useQueueFilter' -import { getLanguageFlag } from 'components/tags/SubjectTag' - -// Tags can be any arbitrary string, and lang tags are prefixed with lang:[code2] so we use this to get the lang code from tag string -const getLangFromTag = (tag: string) => tag.split(':')[1] - -export const QueueFilterLanguage = () => { - const { queueFilters, toggleLanguage, clearLanguages } = useQueueFilter() - const includedLanguages = - queueFilters.tags - ?.filter((tag) => tag.startsWith('lang:')) - .map(getLangFromTag) || [] - const excludedLanguages = - queueFilters.excludeTags - ?.filter((tag) => tag.startsWith('lang:')) - .map(getLangFromTag) || [] - - return ( -
-
-

Language Filters

-
-
- toggleLanguage('include', lang)} - /> - toggleLanguage('exclude', lang)} - /> -
- -

- Note:{' '} - - When multiple languages are selected, only subjects that are tagged - with all of those languages will be included/excluded. - -

- {(includedLanguages.length > 0 || excludedLanguages.length > 0) && ( - { - clearLanguages() - }} - > - Clear Language Filters - - )} -
- ) -} - -const LanguageList = ({ - header, - onSelect, - selected = [], - disabled = [], -}: { - selected: string[] - disabled: string[] - header: string - onSelect: (lang: string) => void -}) => { - return ( -
-

- {header} -

-
- {availableLanguageCodes.map((code2) => { - const isDisabled = disabled.includes(code2) - return ( - - ) - })} -
-
- ) -} diff --git a/components/reports/QueueFilter/Panel.tsx b/components/reports/QueueFilter/Panel.tsx index d8cd8e6..2404c6c 100644 --- a/components/reports/QueueFilter/Panel.tsx +++ b/components/reports/QueueFilter/Panel.tsx @@ -1,6 +1,5 @@ import { Popover, Transition } from '@headlessui/react' import { ChevronDownIcon } from '@heroicons/react/20/solid' -import { QueueFilterLanguage } from './Language' import { QueueFilterSubjectType } from './SubjectType' import { useSearchParams } from 'next/navigation' import { useQueueFilterBuilder } from '../useQueueFilter' diff --git a/components/reports/QueueFilter/Tag.tsx b/components/reports/QueueFilter/Tag.tsx index 4bc799e..d5e910f 100644 --- a/components/reports/QueueFilter/Tag.tsx +++ b/components/reports/QueueFilter/Tag.tsx @@ -111,7 +111,7 @@ export const QueueFilterTags = () => { return (
-

+