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,
EllipsisHorizontalIcon,
PencilSquareIcon,
TrashIcon,
ExclamationTriangleIcon
ChatBubbleBottomCenterTextIcon,
TrashIcon
} from "@heroicons/react/24/outline";
import { queryClient } from "../../App";
@ -87,7 +87,10 @@ export default function Filters({}: FilterProps){
const queryClient = useQueryClient();
const [createFilterIsOpen, toggleCreateFilter] = useToggle(false);
const [createFilterIsOpen, setCreateFilterIsOpen] = useState(false);
const toggleCreateFilter = () => {
setCreateFilterIsOpen(!createFilterIsOpen);
};
const [showImportModal, setShowImportModal] = useState(false);
const [importJson, setImportJson] = useState("");
@ -140,53 +143,71 @@ export default function Filters({}: FilterProps){
}
};
const [showDropdown, setShowDropdown] = useState(false);
return (
<main>
<FilterAddForm isOpen={createFilterIsOpen} toggle={toggleCreateFilter} />
<header className="py-10">
<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">
Filters
</h1>
<h1 className="text-3xl font-bold text-black dark:text-white">Filters</h1>
<div className="relative">
<button
type="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"
onClick={toggleCreateFilter}
>
<PlusIcon className="h-5 w-5 mr-1" />
Add Filter
</button>
<button
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)}
>
<ChevronDownIcon className="h-5 w-5" />
</button>
{showDropdown && (
<div className="absolute right-0 mt-2 w-46 bg-white dark:bg-gray-700 rounded-md shadow-lg z-50">
<button
type="button"
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"
onClick={() => setShowImportModal(true)}
>
Import Filter
</button>
</div>
)}
<Menu>
{({ open }) => (
<>
<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"
onClick={(e: { stopPropagation: () => void; }) => {
if (!open) {
e.stopPropagation();
toggleCreateFilter();
}
}}
>
<PlusIcon className="h-5 w-5 mr-1" />
Add Filter
</button>
<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">
<ChevronDownIcon className="h-5 w-5" />
</Menu.Button>
<Transition
show={open}
enter="transition ease-out duration-100 transform"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="transition ease-in duration-75 transform"
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>
</header>
{showImportModal && (
<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">
<h2 className="text-lg font-medium mb-4 text-black dark:text-white">Import Filter JSON</h2>
<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"
value={importJson}
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"
onClick={() => setShowImportModal(false)}
>
Cancel
Cancel
</button>
<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"
onClick={handleImportJson}
>
Import
Import
</button>
</div>
</div>
</div>
)}
<FilterList toggleCreateFilter={toggleCreateFilter} />
</main>
);
@ -326,8 +346,7 @@ interface FilterItemDropdownProps {
const FilterItemDropdown = ({ filter, onToggle }: FilterItemDropdownProps) => {
// This function handles the export of a filter to a JSON string
const handleExportJson = useCallback(async () => {
try {
const handleExportJson = useCallback(async (discordFormat = false) => { try {
type CompleteFilterType = {
id: number;
name: string;
@ -387,15 +406,19 @@ const FilterItemDropdown = ({ filter, onToggle }: FilterItemDropdownProps) => {
4
);
navigator.clipboard.writeText(json).then(() => {
const finalJson = discordFormat ? "```JSON\n" + json + "\n```" : json;
navigator.clipboard.writeText(finalJson).then(() => {
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} />);
});
} catch (error) {
console.error(error);
toast.custom((t) => <Toast type="error" body="Failed to get filter data." t={t} />);
}
} catch (error) {
console.error(error);
toast.custom((t) => <Toast type="error" body="Failed to get filter data." t={t} />);
}
}, [filter]);
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",
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
)}
onClick={handleExportJson}
>
onClick={() => handleExportJson(false)} >
<ArrowDownTrayIcon
className={classNames(
active ? "text-white" : "text-blue-500",
@ -498,6 +520,26 @@ const FilterItemDropdown = ({ filter, onToggle }: FilterItemDropdownProps) => {
</button>
)}
</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>
{({ active }) => (
<button