fix(filters): close add new dropdown of focus (#777)

* hide dropdown when clicking outside it

* cleaner code

* cleaned up code

removed uneccessary div
changed to focus:ring-inset on buttons

* revert focus-ring-inset change

will handle this for the entire app in a separate branch

* changed to using headlessui

* added transition to the dropdown

* feat: add export JSON to Discord button

The Discord button exports the filter data in JSON format,
but with Discord-specific formatting.

as requested on Discord
This commit is contained in:
soup 2023-03-22 22:00:47 +01:00 committed by GitHub
parent 0564f0bf7a
commit bcd5128c59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -17,8 +17,8 @@ import {
DocumentDuplicateIcon, DocumentDuplicateIcon,
EllipsisHorizontalIcon, EllipsisHorizontalIcon,
PencilSquareIcon, PencilSquareIcon,
TrashIcon, ChatBubbleBottomCenterTextIcon,
ExclamationTriangleIcon TrashIcon
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { queryClient } from "../../App"; import { queryClient } from "../../App";
@ -87,10 +87,13 @@ export default function Filters({}: FilterProps){
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [createFilterIsOpen, toggleCreateFilter] = useToggle(false); const [createFilterIsOpen, setCreateFilterIsOpen] = useState(false);
const toggleCreateFilter = () => {
setCreateFilterIsOpen(!createFilterIsOpen);
};
const [showImportModal, setShowImportModal] = useState(false); const [showImportModal, setShowImportModal] = useState(false);
const [importJson, setImportJson] = useState(""); const [importJson, setImportJson] = useState("");
// This function handles the import of a filter from a JSON string // This function handles the import of a filter from a JSON string
const handleImportJson = async () => { const handleImportJson = async () => {
@ -140,53 +143,71 @@ export default function Filters({}: FilterProps){
} }
}; };
const [showDropdown, setShowDropdown] = useState(false);
return ( return (
<main> <main>
<FilterAddForm isOpen={createFilterIsOpen} toggle={toggleCreateFilter} /> <FilterAddForm isOpen={createFilterIsOpen} toggle={toggleCreateFilter} />
<header className="py-10"> <header className="py-10">
<div className="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between"> <div className="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between">
<h1 className="text-3xl font-bold text-black dark:text-white"> <h1 className="text-3xl font-bold text-black dark:text-white">Filters</h1>
Filters
</h1>
<div className="relative"> <div className="relative">
<button <Menu>
type="button" {({ open }) => (
className="relative inline-flex items-center px-4 py-2 shadow-sm text-sm font-medium rounded-l-md text-white bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-blue-500" <>
onClick={toggleCreateFilter} <button
> className="relative inline-flex items-center px-4 py-2 shadow-sm text-sm font-medium rounded-l-md text-white bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-blue-500"
<PlusIcon className="h-5 w-5 mr-1" /> onClick={(e: { stopPropagation: () => void; }) => {
Add Filter if (!open) {
</button> e.stopPropagation();
<button toggleCreateFilter();
type="button" }
className="relative inline-flex items-center px-2 py-2 border-l border-spacing-1 dark:border-black shadow-sm text-sm font-medium rounded-r-md text-white bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-blue-500" }}
onClick={() => setShowDropdown(!showDropdown)} >
> <PlusIcon className="h-5 w-5 mr-1" />
<ChevronDownIcon className="h-5 w-5" /> Add Filter
</button> </button>
{showDropdown && ( <Menu.Button className="relative inline-flex items-center px-2 py-2 border-l border-spacing-1 dark:border-black shadow-sm text-sm font-medium rounded-r-md text-white bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-blue-500">
<div className="absolute right-0 mt-2 w-46 bg-white dark:bg-gray-700 rounded-md shadow-lg z-50"> <ChevronDownIcon className="h-5 w-5" />
<button </Menu.Button>
type="button" <Transition
className="w-full text-left py-2 px-4 text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-blue-500" show={open}
onClick={() => setShowImportModal(true)} enter="transition ease-out duration-100 transform"
> enterFrom="opacity-0 scale-95"
Import Filter enterTo="opacity-100 scale-100"
</button> leave="transition ease-in duration-75 transform"
</div> leaveFrom="opacity-100 scale-100"
)} leaveTo="opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 mt-0.5 w-46 bg-white dark:bg-gray-700 rounded-md shadow-lg">
<Menu.Item>
{({ active }) => (
<button
type="button"
className={`${
active
? "bg-gray-50 dark:bg-gray-600"
: ""
} w-full text-left py-2 px-4 text-sm font-medium text-gray-700 dark:text-gray-200 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-blue-500`}
onClick={() => setShowImportModal(true)}
>
Import Filter
</button>
)}
</Menu.Item>
</Menu.Items>
</Transition>
</>
)}
</Menu>
</div> </div>
</div> </div>
</header> </header>
{showImportModal && ( {showImportModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="w-1/2 md:w-1/2 bg-white dark:bg-gray-800 p-6 rounded-md shadow-lg"> <div className="w-1/2 md:w-1/2 bg-white dark:bg-gray-800 p-6 rounded-md shadow-lg">
<h2 className="text-lg font-medium mb-4 text-black dark:text-white">Import Filter JSON</h2> <h2 className="text-lg font-medium mb-4 text-black dark:text-white">Import Filter JSON</h2>
<textarea <textarea
className="form-input block w-full resize-y rounded-md border-gray-300 dark:bg-gray-800 dark:border-gray-600 shadow-sm text-sm font-medium text-gray-700 dark:text-white focus:outline-none focus:ring-2 focus:ring-offset-0 focus:ring-blue-500 dark:focus:ring-blue-500 mb-4" className="form-input block w-full resize-y rounded-md border-gray-300 dark:bg-gray-800 dark:border-gray-600 shadow-sm text-sm font-medium text-gray-700 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-500 mb-4"
placeholder="Paste JSON data here" placeholder="Paste JSON data here"
value={importJson} value={importJson}
onChange={(event) => setImportJson(event.target.value)} onChange={(event) => setImportJson(event.target.value)}
@ -198,20 +219,19 @@ export default function Filters({}: FilterProps){
className="bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-blue-500" className="bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-blue-500"
onClick={() => setShowImportModal(false)} onClick={() => setShowImportModal(false)}
> >
Cancel Cancel
</button> </button>
<button <button
type="button" type="button"
className="ml-4 relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" className="ml-4 relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
onClick={handleImportJson} onClick={handleImportJson}
> >
Import Import
</button> </button>
</div> </div>
</div> </div>
</div> </div>
)} )}
<FilterList toggleCreateFilter={toggleCreateFilter} /> <FilterList toggleCreateFilter={toggleCreateFilter} />
</main> </main>
); );
@ -326,8 +346,7 @@ interface FilterItemDropdownProps {
const FilterItemDropdown = ({ filter, onToggle }: FilterItemDropdownProps) => { const FilterItemDropdown = ({ filter, onToggle }: FilterItemDropdownProps) => {
// This function handles the export of a filter to a JSON string // This function handles the export of a filter to a JSON string
const handleExportJson = useCallback(async () => { const handleExportJson = useCallback(async (discordFormat = false) => { try {
try {
type CompleteFilterType = { type CompleteFilterType = {
id: number; id: number;
name: string; name: string;
@ -386,16 +405,20 @@ const FilterItemDropdown = ({ filter, onToggle }: FilterItemDropdownProps) => {
null, null,
4 4
); );
const finalJson = discordFormat ? "```JSON\n" + json + "\n```" : json;
navigator.clipboard.writeText(json).then(() => { navigator.clipboard.writeText(finalJson).then(() => {
toast.custom((t) => <Toast type="success" body="Filter copied to clipboard." t={t} />); toast.custom((t) => <Toast type="success" body="Filter copied to clipboard." t={t} />);
}, () => { }, () => {
toast.custom((t) => <Toast type="error" body="Failed to copy JSON to clipboard." t={t} />); toast.custom((t) => <Toast type="error" body="Failed to copy JSON to clipboard." t={t} />);
}); });
} catch (error) {
console.error(error); } catch (error) {
toast.custom((t) => <Toast type="error" body="Failed to get filter data." t={t} />); console.error(error);
} toast.custom((t) => <Toast type="error" body="Failed to get filter data." t={t} />);
}
}, [filter]); }, [filter]);
const cancelModalButtonRef = useRef(null); const cancelModalButtonRef = useRef(null);
@ -485,8 +508,7 @@ const FilterItemDropdown = ({ filter, onToggle }: FilterItemDropdownProps) => {
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300", active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm" "font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
)} )}
onClick={handleExportJson} onClick={() => handleExportJson(false)} >
>
<ArrowDownTrayIcon <ArrowDownTrayIcon
className={classNames( className={classNames(
active ? "text-white" : "text-blue-500", active ? "text-white" : "text-blue-500",
@ -498,6 +520,26 @@ const FilterItemDropdown = ({ filter, onToggle }: FilterItemDropdownProps) => {
</button> </button>
)} )}
</Menu.Item> </Menu.Item>
<Menu.Item>
{({ active }) => (
<button
className={classNames(
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
)}
onClick={() => handleExportJson(true)}
>
<ChatBubbleBottomCenterTextIcon
className={classNames(
active ? "text-white" : "text-blue-500",
"w-5 h-5 mr-2"
)}
aria-hidden="true"
/>
Export JSON to Discord
</button>
)}
</Menu.Item>
<Menu.Item> <Menu.Item>
{({ active }) => ( {({ active }) => (
<button <button