-
@@ -360,7 +357,6 @@ export function General() {
-
);
}
diff --git a/web/src/screens/filters/list.tsx b/web/src/screens/filters/list.tsx
index 1856968..f679cc4 100644
--- a/web/src/screens/filters/list.tsx
+++ b/web/src/screens/filters/list.tsx
@@ -3,15 +3,22 @@ import { Link } from "react-router-dom";
import { toast } from "react-hot-toast";
import { Listbox, Menu, Switch, Transition } from "@headlessui/react";
import { useMutation, useQuery, useQueryClient } from "react-query";
+import { FormikValues } from "formik";
+import { useCallback } from "react";
+
+import { Tooltip } from "react-tooltip";
+
import {
ArrowsRightLeftIcon,
CheckIcon,
ChevronDownIcon,
+ PlusIcon,
DocumentDuplicateIcon,
EllipsisHorizontalIcon,
PencilSquareIcon,
- TrashIcon
+ TrashIcon,
+ ExclamationTriangleIcon
} from "@heroicons/react/24/outline";
import { queryClient } from "../../App";
@@ -22,6 +29,7 @@ import { APIClient } from "../../api/APIClient";
import Toast from "../../components/notifications/Toast";
import { EmptyListState } from "../../components/emptystates";
import { DeleteModal } from "../../components/modals";
+import { ArrowDownTrayIcon } from "@heroicons/react/24/solid";
type FilterListState = {
indexerFilter: string[],
@@ -71,9 +79,69 @@ const FilterListReducer = (state: FilterListState, action: Actions): FilterListS
}
};
-export default function Filters() {
+interface FilterProps {
+ values?: FormikValues;
+}
+
+export default function Filters({}: FilterProps){
+
+ const queryClient = useQueryClient();
+
const [createFilterIsOpen, toggleCreateFilter] = useToggle(false);
+ const [showImportModal, setShowImportModal] = useState(false);
+ const [importJson, setImportJson] = useState("");
+
+ // This function handles the import of a filter from a JSON string
+ const handleImportJson = async () => {
+ try {
+ const importedData = JSON.parse(importJson);
+
+ // Extract the filter data and name from the imported object
+ const importedFilter = importedData.data;
+ const filterName = importedData.name;
+
+ // Check if the required properties are present and add them with default values if they are missing
+ const requiredProperties = ["resolutions", "sources", "codecs", "containers"];
+ requiredProperties.forEach((property) => {
+ if (!importedFilter.hasOwnProperty(property)) {
+ importedFilter[property] = [];
+ }
+ });
+
+ // Fetch existing filters from the API
+ const existingFilters = await APIClient.filters.getAll();
+
+ // Create a unique filter title by appending an incremental number if title is taken by another filter
+ let nameCounter = 0;
+ let uniqueFilterName = filterName;
+ while (existingFilters.some((filter) => filter.name === uniqueFilterName)) {
+ nameCounter++;
+ uniqueFilterName = `${filterName}-${nameCounter}`;
+ }
+
+ // Create a new filter using the API
+ const newFilter: Filter = {
+ ...importedFilter,
+ name: uniqueFilterName
+ };
+
+ await APIClient.filters.create(newFilter);
+
+ // Update the filter list
+ queryClient.invalidateQueries("filters");
+
+ toast.custom((t) =>
);
+ setShowImportModal(false);
+ } catch (error) {
+ // Log the error and show an error toast message
+ console.error("Error:", error);
+ toast.custom((t) =>
);
+ }
+ };
+
+ const [showDropdown, setShowDropdown] = useState(false);
+
return (
@@ -81,19 +149,68 @@ export default function Filters() {
- Filters
+ Filters
-
+
+
+ {showDropdown && (
+
+
+
+ )}
+ {showImportModal && (
+
+ )}
@@ -157,7 +274,7 @@ function FilterList({ toggleCreateFilter }: any) {
{data && data.length > 0 ? (
{filtered.filtered.map((filter: Filter, idx) => (
-
+
))}
) : (
@@ -201,14 +318,86 @@ const StatusButton = ({ data, label, value, currentValue, dispatch }: StatusButt
};
interface FilterItemDropdownProps {
+ values: FormikValues;
filter: Filter;
onToggle: (newState: boolean) => void;
}
-const FilterItemDropdown = ({
- filter,
- onToggle
-}: FilterItemDropdownProps) => {
+const FilterItemDropdown = ({ filter, onToggle }: FilterItemDropdownProps) => {
+
+ // This function handles the export of a filter to a JSON string
+ const handleExportJson = useCallback(async () => {
+ try {
+ type CompleteFilterType = {
+ id: number;
+ name: string;
+ created_at: Date;
+ updated_at: Date;
+ indexers: any;
+ actions: any;
+ actions_count: any;
+ external_script_enabled: any;
+ external_script_cmd: any;
+ external_script_args: any;
+ external_script_expect_status: any;
+ external_webhook_enabled: any;
+ external_webhook_host: any;
+ external_webhook_data: any;
+ external_webhook_expect_status: any;
+ };
+
+ const completeFilter = await APIClient.filters.getByID(filter.id) as Partial
;
+
+ // Extract the filter name and remove unwanted properties
+ const title = completeFilter.name;
+ delete completeFilter.name;
+ delete completeFilter.id;
+ delete completeFilter.created_at;
+ delete completeFilter.updated_at;
+ delete completeFilter.actions_count;
+ delete completeFilter.indexers;
+ delete completeFilter.actions;
+ delete completeFilter.external_script_enabled;
+ delete completeFilter.external_script_cmd;
+ delete completeFilter.external_script_args;
+ delete completeFilter.external_script_expect_status;
+ delete completeFilter.external_webhook_enabled;
+ delete completeFilter.external_webhook_host;
+ delete completeFilter.external_webhook_data;
+ delete completeFilter.external_webhook_expect_status;
+
+ // Remove properties with default values from the exported filter to minimize the size of the JSON string
+ ["enabled", "priority", "smart_episode", "resolutions", "sources", "codecs", "containers"].forEach((key) => {
+ const value = completeFilter[key as keyof CompleteFilterType];
+ if (["enabled", "priority", "smart_episode"].includes(key) && (value === false || value === 0)) {
+ delete completeFilter[key as keyof CompleteFilterType];
+ } else if (["resolutions", "sources", "codecs", "containers"].includes(key) && Array.isArray(value) && value.length === 0) {
+ delete completeFilter[key as keyof CompleteFilterType];
+ }
+ });
+
+ // Create a JSON string from the filter data, including a name and version
+ const json = JSON.stringify(
+ {
+ "name": title,
+ "version": "1.0",
+ data: completeFilter
+ },
+ null,
+ 4
+ );
+
+ navigator.clipboard.writeText(json).then(() => {
+ toast.custom((t) => );
+ }, () => {
+ toast.custom((t) => );
+ });
+ } catch (error) {
+ console.error(error);
+ toast.custom((t) => );
+ }
+ }, [filter]);
+
const cancelModalButtonRef = useRef(null);
const queryClient = useQueryClient();
@@ -289,6 +478,26 @@ const FilterItemDropdown = ({
)}
+
+ {({ active }) => (
+
+ )}
+
{({ active }) => (