mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 16:59:12 +00:00
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:
parent
0564f0bf7a
commit
bcd5128c59
1 changed files with 91 additions and 49 deletions
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue