refactor(web): replace pkg react-query with tanstack/react-query (#868)

* refactor: move to tanstack/react-query and fix cache

* refactor(releases): move to tanstack/react-query

* refactor(logs): move to tanstack/react-query

* refactor(base): move to tanstack/react-query

* refactor(base): move to tanstack/react-query

* refactor(dashboard): move to tanstack/react-query

* refactor(auth): move to tanstack/react-query

* refactor(filters): move to tanstack/react-query

* refactor(settings): move to tanstack/react-query

* chore(pkg): add tanstack/react-query

* refactor(filters): move to tanstack/react-query

* refactor: move to tanstack/react-query

* refactor: invalidate queries

* chore(pkg): remove old react-query

* chore: change imports to root prefixes

* build: remove needs web from test

* set enableReinitialize to true to fix formik caching issues

* fix all property for apiKeys const

* fix toast when enabling/disabling feed

---------

Co-authored-by: martylukyy <35452459+martylukyy@users.noreply.github.com>
This commit is contained in:
ze0s 2023-04-27 21:26:27 +02:00 committed by GitHub
parent 0be92bef65
commit 6e5385a490
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 1101 additions and 1117 deletions

View file

@ -44,19 +44,12 @@ jobs:
test: test:
name: Test name: Test
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: web
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Download web production build
uses: actions/download-artifact@v3
with:
name: web-dist
path: web/dist
# 1.20 is the last version to support Windows < 10, Server < 2016, and MacOS < 1.15. # 1.20 is the last version to support Windows < 10, Server < 2016, and MacOS < 1.15.
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v4
@ -107,7 +100,7 @@ jobs:
path: dist/* path: dist/*
goreleaser: goreleaser:
name: Build & publish binaries and images name: Build and publish binaries
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [web, test] needs: [web, test]

View file

@ -11,6 +11,8 @@
"@headlessui/react": "^1.6.4", "@headlessui/react": "^1.6.4",
"@heroicons/react": "^2.0.11", "@heroicons/react": "^2.0.11",
"@hookform/error-message": "^2.0.0", "@hookform/error-message": "^2.0.0",
"@tanstack/react-query": "^4.29.3",
"@tanstack/react-query-devtools": "^4.29.3",
"date-fns": "^2.28.0", "date-fns": "^2.28.0",
"formik": "^2.2.9", "formik": "^2.2.9",
"react": "^18.2.0", "react": "^18.2.0",
@ -22,7 +24,6 @@
"react-multi-select-component": "^4.2.9", "react-multi-select-component": "^4.2.9",
"react-popper-tooltip": "^4.4.2", "react-popper-tooltip": "^4.4.2",
"react-portal": "^4.2.2", "react-portal": "^4.2.2",
"react-query": "^3.39.1",
"react-ridge-state": "4.2.2", "react-ridge-state": "4.2.2",
"react-router-dom": "^6.3.0", "react-router-dom": "^6.3.0",
"react-select": "^5.3.2", "react-select": "^5.3.2",

View file

@ -1,5 +1,5 @@
import { QueryClient, QueryClientProvider, useQueryErrorResetBoundary } from "react-query"; import { QueryClient, QueryClientProvider, useQueryErrorResetBoundary } from "@tanstack/react-query";
import { ReactQueryDevtools } from "react-query/devtools"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ErrorBoundary } from "react-error-boundary"; import { ErrorBoundary } from "react-error-boundary";
import { toast, Toaster } from "react-hot-toast"; import { toast, Toaster } from "react-hot-toast";

View file

@ -34,6 +34,8 @@ export async function HttpClient<T>(
// Show an error toast to notify the user what occurred // Show an error toast to notify the user what occurred
return Promise.reject(new Error("Unauthorized")); return Promise.reject(new Error("Unauthorized"));
} else if (response.status === 404) {
return Promise.reject(new Error("Not found"));
} }
return Promise.reject(new Error(await response.text())); return Promise.reject(new Error(await response.text()));
@ -50,7 +52,7 @@ export async function HttpClient<T>(
const appClient = { const appClient = {
Get: <T>(endpoint: string) => HttpClient<T>(endpoint, "GET"), Get: <T>(endpoint: string) => HttpClient<T>(endpoint, "GET"),
Post: <T = void>(endpoint: string, data: PostBody = undefined) => HttpClient<T>(endpoint, "POST", { body: data }), Post: <T = void>(endpoint: string, data: PostBody = undefined) => HttpClient<T>(endpoint, "POST", { body: data }),
Put: (endpoint: string, data: PostBody) => HttpClient<void>(endpoint, "PUT", { body: data }), Put: <T = void>(endpoint: string, data: PostBody) => HttpClient<T>(endpoint, "PUT", { body: data }),
Patch: (endpoint: string, data: PostBody = undefined) => HttpClient<void>(endpoint, "PATCH", { body: data }), Patch: (endpoint: string, data: PostBody = undefined) => HttpClient<void>(endpoint, "PATCH", { body: data }),
Delete: (endpoint: string) => HttpClient<void>(endpoint, "DELETE") Delete: (endpoint: string) => HttpClient<void>(endpoint, "DELETE")
}; };
@ -113,7 +115,7 @@ export const APIClient = {
}, },
getByID: (id: number) => appClient.Get<Filter>(`api/filters/${id}`), getByID: (id: number) => appClient.Get<Filter>(`api/filters/${id}`),
create: (filter: Filter) => appClient.Post<Filter>("api/filters", filter), create: (filter: Filter) => appClient.Post<Filter>("api/filters", filter),
update: (filter: Filter) => appClient.Put(`api/filters/${filter.id}`, filter), update: (filter: Filter) => appClient.Put<Filter>(`api/filters/${filter.id}`, filter),
duplicate: (id: number) => appClient.Get<Filter>(`api/filters/${id}/duplicate`), duplicate: (id: number) => appClient.Get<Filter>(`api/filters/${id}/duplicate`),
toggleEnable: (id: number, enabled: boolean) => appClient.Put(`api/filters/${id}/enabled`, { enabled }), toggleEnable: (id: number, enabled: boolean) => appClient.Put(`api/filters/${id}/enabled`, { enabled }),
delete: (id: number) => appClient.Delete(`api/filters/${id}`) delete: (id: number) => appClient.Delete(`api/filters/${id}`)

View file

@ -1,5 +1,5 @@
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import logo from "@/logo.png"; import logo from "@app/logo.png";
export const NotFound = () => { export const NotFound = () => {
return ( return (

View file

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { classNames } from "../../utils"; import { classNames } from "@utils";
interface ButtonProps { interface ButtonProps {
className?: string; className?: string;

View file

@ -3,7 +3,7 @@ import { formatDistanceToNowStrict } from "date-fns";
import { CheckIcon } from "@heroicons/react/24/solid"; import { CheckIcon } from "@heroicons/react/24/solid";
import { ClockIcon, ExclamationCircleIcon, NoSymbolIcon } from "@heroicons/react/24/outline"; import { ClockIcon, ExclamationCircleIcon, NoSymbolIcon } from "@heroicons/react/24/outline";
import { classNames, simplifyDate } from "../../utils"; import { classNames, simplifyDate } from "@utils";
import { Tooltip } from "../tooltips/Tooltip"; import { Tooltip } from "../tooltips/Tooltip";
interface CellProps { interface CellProps {

View file

@ -1,5 +1,5 @@
import { FC } from "react"; import { FC } from "react";
import { SettingsContext } from "../utils/Context"; import { SettingsContext } from "@utils/Context";
interface DebugProps { interface DebugProps {
values: unknown; values: unknown;

View file

@ -1,4 +1,4 @@
import { useToggle } from "../../hooks/hooks"; import { useToggle } from "@hooks/hooks";
import { CheckIcon, DocumentDuplicateIcon, EyeIcon, EyeSlashIcon } from "@heroicons/react/24/outline"; import { CheckIcon, DocumentDuplicateIcon, EyeIcon, EyeSlashIcon } from "@heroicons/react/24/outline";
import { useState } from "react"; import { useState } from "react";

View file

@ -1,6 +1,6 @@
import { Field, FieldProps } from "formik"; import { Field, FieldProps } from "formik";
import { classNames } from "../../utils"; import { classNames } from "@utils";
import { CustomTooltip } from "../tooltips/CustomTooltip"; import { CustomTooltip } from "@components/tooltips/CustomTooltip";
interface ErrorFieldProps { interface ErrorFieldProps {
name: string; name: string;

View file

@ -1,8 +1,8 @@
import { Field, FieldProps, useFormikContext } from "formik"; import { Field, FieldProps, useFormikContext } from "formik";
import { classNames } from "../../utils"; import { classNames } from "@utils";
import { EyeIcon, EyeSlashIcon, CheckCircleIcon, XCircleIcon } from "@heroicons/react/24/solid"; import { EyeIcon, EyeSlashIcon, CheckCircleIcon, XCircleIcon } from "@heroicons/react/24/solid";
import { useToggle } from "../../hooks/hooks"; import { useToggle } from "@hooks/hooks";
import { CustomTooltip } from "../tooltips/CustomTooltip"; import { CustomTooltip } from "@components/tooltips/CustomTooltip";
import { useEffect } from "react"; import { useEffect } from "react";
type COL_WIDTHS = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; type COL_WIDTHS = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;

View file

@ -1,13 +1,13 @@
import type { FieldProps, FieldValidator } from "formik"; import type { FieldProps, FieldValidator } from "formik";
import { Field } from "formik"; import { Field } from "formik";
import { classNames } from "../../utils"; import { classNames } from "@utils";
import { useToggle } from "../../hooks/hooks"; import { useToggle } from "@hooks/hooks";
import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/solid"; import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/solid";
import { Switch } from "@headlessui/react"; import { Switch } from "@headlessui/react";
import { ErrorField, RequiredField } from "./common"; import { ErrorField, RequiredField } from "./common";
import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select"; import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select";
import { SelectFieldProps } from "./select"; import { SelectFieldProps } from "./select";
import { CustomTooltip } from "../tooltips/CustomTooltip"; import { CustomTooltip } from "@components/tooltips/CustomTooltip";
interface TextFieldWideProps { interface TextFieldWideProps {
name: string; name: string;

View file

@ -1,6 +1,6 @@
import { Field, useFormikContext } from "formik"; import { Field, useFormikContext } from "formik";
import { RadioGroup } from "@headlessui/react"; import { RadioGroup } from "@headlessui/react";
import { classNames } from "../../utils"; import { classNames } from "@utils";
export interface radioFieldsetOption { export interface radioFieldsetOption {
label: string; label: string;

View file

@ -4,9 +4,9 @@ import { Listbox, Transition } from "@headlessui/react";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/24/solid"; import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/24/solid";
import { MultiSelect as RMSC } from "react-multi-select-component"; import { MultiSelect as RMSC } from "react-multi-select-component";
import { classNames, COL_WIDTHS } from "../../utils"; import { classNames, COL_WIDTHS } from "@utils";
import { SettingsContext } from "../../utils/Context"; import { SettingsContext } from "@utils/Context";
import { CustomTooltip } from "../tooltips/CustomTooltip"; import { CustomTooltip } from "@components/tooltips/CustomTooltip";
export interface MultiSelectOption { export interface MultiSelectOption {
value: string | number; value: string | number;

View file

@ -1,7 +1,7 @@
import type { FieldProps } from "formik"; import type { FieldProps } from "formik";
import { Field } from "formik"; import { Field } from "formik";
import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select"; import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select";
import { OptionBasicTyped } from "../../domain/constants"; import { OptionBasicTyped } from "@domain/constants";
import CreatableSelect from "react-select/creatable"; import CreatableSelect from "react-select/creatable";
import { CustomTooltip } from "../tooltips/CustomTooltip"; import { CustomTooltip } from "../tooltips/CustomTooltip";

View file

@ -2,7 +2,8 @@ import React from "react";
import type { FieldInputProps, FieldMetaProps, FieldProps, FormikProps, FormikValues } from "formik"; import type { FieldInputProps, FieldMetaProps, FieldProps, FormikProps, FormikValues } from "formik";
import { Field } from "formik"; import { Field } from "formik";
import { Switch as HeadlessSwitch } from "@headlessui/react"; import { Switch as HeadlessSwitch } from "@headlessui/react";
import { classNames } from "../../utils";
import { classNames } from "@utils";
import { CustomTooltip } from "../tooltips/CustomTooltip"; import { CustomTooltip } from "../tooltips/CustomTooltip";
type SwitchProps<V = unknown> = { type SwitchProps<V = unknown> = {
@ -82,7 +83,7 @@ const SwitchGroup = ({
}: SwitchGroupProps) => ( }: SwitchGroupProps) => (
<HeadlessSwitch.Group as="ol" className="py-4 flex items-center justify-between"> <HeadlessSwitch.Group as="ol" className="py-4 flex items-center justify-between">
{label && <div className="flex flex-col"> {label && <div className="flex flex-col">
<HeadlessSwitch.Label as={heading ? "h2" : "p"} className={classNames("flex float-left cursor-default mb-2 text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide", heading ? "text-lg" : "text-sm")} <HeadlessSwitch.Label as={heading ? "h2" : "span"} className={classNames("flex float-left cursor-default mb-2 text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide", heading ? "text-lg" : "text-sm")}
passive> passive>
<div className="flex"> <div className="flex">
{label} {label}

View file

@ -1,7 +1,7 @@
import React, { FC, forwardRef, ReactNode } from "react"; import React, { FC, forwardRef, ReactNode } from "react";
import { DeepMap, FieldError, Path, RegisterOptions, UseFormRegister } from "react-hook-form"; import { DeepMap, FieldError, Path, RegisterOptions, UseFormRegister } from "react-hook-form";
import { classNames, get } from "../../utils"; import { classNames, get } from "@utils";
import { useToggle } from "../../hooks/hooks"; import { useToggle } from "@hooks/hooks";
import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/solid"; import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/solid";
import { ErrorMessage } from "@hookform/error-message"; import { ErrorMessage } from "@hookform/error-message";
import type { FieldValues } from "react-hook-form"; import type { FieldValues } from "react-hook-form";

View file

@ -1,7 +1,7 @@
import { FC } from "react"; import { FC } from "react";
import { CheckCircleIcon, ExclamationCircleIcon, ExclamationTriangleIcon, XMarkIcon } from "@heroicons/react/24/solid"; import { CheckCircleIcon, ExclamationCircleIcon, ExclamationTriangleIcon, XMarkIcon } from "@heroicons/react/24/solid";
import { toast, Toast as Tooast } from "react-hot-toast"; import { toast, Toast as Tooast } from "react-hot-toast";
import { classNames } from "../../utils"; import { classNames } from "@utils";
type Props = { type Props = {
type: "error" | "success" | "warning" type: "error" | "success" | "warning"
@ -26,7 +26,7 @@ const Toast: FC<Props> = ({ type, body, t }) => (
{type === "error" && "Error"} {type === "error" && "Error"}
{type === "warning" && "Warning"} {type === "warning" && "Warning"}
</p> </p>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{body}</p> <span className="mt-1 text-sm text-gray-500 dark:text-gray-400">{body}</span>
</div> </div>
<div className="ml-4 flex-shrink-0 flex"> <div className="ml-4 flex-shrink-0 flex">
<button <button

View file

@ -3,10 +3,11 @@ import { XMarkIcon } from "@heroicons/react/24/solid";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { Form, Formik } from "formik"; import { Form, Formik } from "formik";
import type { FormikValues } from "formik"; import type { FormikValues } from "formik";
import DEBUG from "../debug"; import DEBUG from "../debug";
import { useToggle } from "../../hooks/hooks"; import { useToggle } from "@hooks/hooks";
import { DeleteModal } from "../modals"; import { DeleteModal } from "../modals";
import { classNames } from "../../utils"; import { classNames } from "@utils";
interface SlideOverProps<DataType> { interface SlideOverProps<DataType> {
title: string; title: string;

View file

@ -1,7 +1,7 @@
import * as React from "react"; import * as React from "react";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { usePopperTooltip } from "react-popper-tooltip"; import { usePopperTooltip } from "react-popper-tooltip";
import { classNames } from "../../utils"; import { classNames } from "@utils";
interface TooltipProps { interface TooltipProps {
label: ReactNode; label: ReactNode;

View file

@ -1,4 +1,4 @@
import { MultiSelectOption } from "@/components/inputs/select"; import { MultiSelectOption } from "@components/inputs/select";
export const resolutions = [ export const resolutions = [
"2160p", "2160p",

View file

@ -1,13 +1,13 @@
import { BrowserRouter, Route, Routes } from "react-router-dom"; import { BrowserRouter, Route, Routes } from "react-router-dom";
import { Login } from "../screens/auth/login"; import { Login } from "@screens/auth/login";
import { Onboarding } from "../screens/auth/onboarding"; import { Onboarding } from "@screens/auth/onboarding";
import Base from "../screens/Base"; import Base from "@screens/Base";
import { Dashboard } from "../screens/dashboard"; import { Dashboard } from "@screens/dashboard";
import { FilterDetails, Filters } from "../screens/filters"; import { FilterDetails, Filters } from "@screens/filters";
import { Logs } from "../screens/Logs"; import { Logs } from "@screens/Logs";
import { Releases } from "../screens/releases"; import { Releases } from "@screens/releases";
import Settings from "../screens/Settings"; import Settings from "@screens/Settings";
import { import {
APISettings, APISettings,
ApplicationSettings, ApplicationSettings,
@ -18,11 +18,11 @@ import {
LogSettings, LogSettings,
NotificationSettings, NotificationSettings,
ReleaseSettings ReleaseSettings
} from "../screens/settings/index"; } from "@screens/settings/index";
import { RegexPlayground } from "../screens/settings/RegexPlayground"; import { RegexPlayground } from "@screens/settings/RegexPlayground";
import { NotFound } from "@/components/alerts/NotFound"; import { NotFound } from "@components/alerts/NotFound";
import { baseUrl } from "../utils"; import { baseUrl } from "@utils";
export const LocalRouter = ({ isLoggedIn }: { isLoggedIn: boolean }) => ( export const LocalRouter = ({ isLoggedIn }: { isLoggedIn: boolean }) => (
<BrowserRouter basename={baseUrl()}> <BrowserRouter basename={baseUrl()}>

View file

@ -1,16 +1,17 @@
import { Fragment } from "react"; import { Fragment } from "react";
import { useMutation, useQueryClient } from "react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { XMarkIcon } from "@heroicons/react/24/solid"; import { XMarkIcon } from "@heroicons/react/24/solid";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import type { FieldProps } from "formik"; import type { FieldProps } from "formik";
import { Field, Form, Formik, FormikErrors, FormikValues } from "formik"; import { Field, Form, Formik, FormikErrors, FormikValues } from "formik";
import { APIClient } from "../../api/APIClient";
import DEBUG from "../../components/debug";
import Toast from "../../components/notifications/Toast";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { APIClient } from "@api/APIClient";
import DEBUG from "@components/debug";
import Toast from "@components/notifications/Toast";
import { filterKeys } from "@screens/filters/list";
interface filterAddFormProps { interface filterAddFormProps {
isOpen: boolean; isOpen: boolean;
toggle: () => void; toggle: () => void;
@ -19,11 +20,11 @@ interface filterAddFormProps {
function FilterAddForm({ isOpen, toggle }: filterAddFormProps) { function FilterAddForm({ isOpen, toggle }: filterAddFormProps) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const navigate = useNavigate(); const navigate = useNavigate();
const mutation = useMutation( const mutation = useMutation({
(filter: Filter) => APIClient.filters.create(filter), mutationFn: (filter: Filter) => APIClient.filters.create(filter),
{
onSuccess: (filter) => { onSuccess: (filter) => {
queryClient.invalidateQueries("filters"); queryClient.invalidateQueries({ queryKey: filterKeys.lists() });
toast.custom((t) => <Toast type="success" body={`Filter ${filter.name} was added`} t={t} />); toast.custom((t) => <Toast type="success" body={`Filter ${filter.name} was added`} t={t} />);
toggle(); toggle();
@ -31,10 +32,10 @@ function FilterAddForm({ isOpen, toggle }: filterAddFormProps) {
navigate(filter.id.toString()); navigate(filter.id.toString());
} }
} }
} });
);
const handleSubmit = (data: unknown) => mutation.mutate(data as Filter); const handleSubmit = (data: unknown) => mutation.mutate(data as Filter);
const validate = (values: FormikValues) => { const validate = (values: FormikValues) => {
const errors = {} as FormikErrors<FormikValues>; const errors = {} as FormikErrors<FormikValues>;
if (!values.name) { if (!values.name) {

View file

@ -1,14 +1,15 @@
import { Fragment } from "react"; import { Fragment } from "react";
import { useMutation, useQueryClient } from "react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { XMarkIcon } from "@heroicons/react/24/solid"; import { XMarkIcon } from "@heroicons/react/24/solid";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import type { FieldProps } from "formik"; import type { FieldProps } from "formik";
import { Field, Form, Formik, FormikErrors, FormikValues } from "formik"; import { Field, Form, Formik, FormikErrors, FormikValues } from "formik";
import { APIClient } from "../../api/APIClient"; import { APIClient } from "@api/APIClient";
import DEBUG from "../../components/debug"; import DEBUG from "@components/debug";
import Toast from "../../components/notifications/Toast"; import Toast from "@components/notifications/Toast";
import { apiKeys } from "@screens/settings/Api";
interface apiKeyAddFormProps { interface apiKeyAddFormProps {
isOpen: boolean; isOpen: boolean;
@ -18,17 +19,16 @@ interface apiKeyAddFormProps {
function APIKeyAddForm({ isOpen, toggle }: apiKeyAddFormProps) { function APIKeyAddForm({ isOpen, toggle }: apiKeyAddFormProps) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const mutation = useMutation( const mutation = useMutation({
(apikey: APIKey) => APIClient.apikeys.create(apikey), mutationFn: (apikey: APIKey) => APIClient.apikeys.create(apikey),
{
onSuccess: (_, key) => { onSuccess: (_, key) => {
queryClient.invalidateQueries("apikeys"); queryClient.invalidateQueries({ queryKey: apiKeys.lists() });
toast.custom((t) => <Toast type="success" body={`API key ${key.name} was added`} t={t}/>); toast.custom((t) => <Toast type="success" body={`API key ${key.name} was added`} t={t}/>);
toggle(); toggle();
} }
} });
);
const handleSubmit = (data: unknown) => mutation.mutate(data as APIKey); const handleSubmit = (data: unknown) => mutation.mutate(data as APIKey);
const validate = (values: FormikValues) => { const validate = (values: FormikValues) => {
@ -56,7 +56,6 @@ function APIKeyAddForm({ isOpen, toggle }: apiKeyAddFormProps) {
leaveTo="translate-x-full" leaveTo="translate-x-full"
> >
<div className="w-screen max-w-2xl border-l dark:border-gray-700"> <div className="w-screen max-w-2xl border-l dark:border-gray-700">
<Formik <Formik
initialValues={{ initialValues={{
name: "", name: "",
@ -114,10 +113,7 @@ function APIKeyAddForm({ isOpen, toggle }: apiKeyAddFormProps) {
type="text" type="text"
className="block w-full shadow-sm dark:bg-gray-800 border-gray-300 dark:border-gray-700 sm:text-sm dark:text-white focus:ring-blue-500 dark:focus:ring-blue-500 focus:border-blue-500 dark:focus:border-blue-500 rounded-md" className="block w-full shadow-sm dark:bg-gray-800 border-gray-300 dark:border-gray-700 sm:text-sm dark:text-white focus:ring-blue-500 dark:focus:ring-blue-500 focus:border-blue-500 dark:focus:border-blue-500 rounded-md"
/> />
{meta.touched && meta.error && <span className="block mt-2 text-red-500">{meta.error}</span>}
{meta.touched && meta.error &&
<span className="block mt-2 text-red-500">{meta.error}</span>}
</div> </div>
)} )}
</Field> </Field>

View file

@ -1,26 +1,26 @@
import React, { Fragment, useRef, useState } from "react"; import React, { Fragment, useRef, useState } from "react";
import { useMutation, useQueryClient } from "react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { XMarkIcon } from "@heroicons/react/24/solid"; import { XMarkIcon } from "@heroicons/react/24/solid";
import { classNames, sleep } from "../../utils";
import { Form, Formik, useFormikContext } from "formik"; import { Form, Formik, useFormikContext } from "formik";
import DEBUG from "../../components/debug";
import { APIClient } from "../../api/APIClient";
import { DownloadClientTypeOptions, DownloadRuleConditionOptions } from "../../domain/constants";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import Toast from "../../components/notifications/Toast";
import { useToggle } from "../../hooks/hooks"; import { classNames, sleep } from "@utils";
import { DeleteModal } from "../../components/modals"; import DEBUG from "@components/debug";
import { APIClient } from "@api/APIClient";
import { DownloadClientTypeOptions, DownloadRuleConditionOptions } from "@domain/constants";
import Toast from "@components/notifications/Toast";
import { useToggle } from "@hooks/hooks";
import { DeleteModal } from "@components/modals";
import { import {
NumberFieldWide, NumberFieldWide,
PasswordFieldWide, PasswordFieldWide,
RadioFieldsetWide, RadioFieldsetWide,
SwitchGroupWide, SwitchGroupWide,
TextFieldWide TextFieldWide
} from "../../components/inputs"; } from "@components/inputs";
import DownloadClient from "../../screens/settings/DownloadClient"; import DownloadClient, { clientKeys } from "@screens/settings/DownloadClient";
import { SelectFieldWide } from "../../components/inputs/input_wide"; import { SelectFieldWide } from "@components/inputs/input_wide";
interface InitialValuesSettings { interface InitialValuesSettings {
basic?: { basic?: {
@ -517,11 +517,10 @@ export function DownloadClientAddForm({ isOpen, toggle }: formProps) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const mutation = useMutation( const addMutation = useMutation({
(client: DownloadClient) => APIClient.download_clients.create(client), mutationFn: (client: DownloadClient) => APIClient.download_clients.create(client),
{
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["downloadClients"]); queryClient.invalidateQueries({ queryKey: clientKeys.lists() });
toast.custom((t) => <Toast type="success" body="Client was added" t={t}/>); toast.custom((t) => <Toast type="success" body="Client was added" t={t}/>);
toggle(); toggle();
@ -529,12 +528,12 @@ export function DownloadClientAddForm({ isOpen, toggle }: formProps) {
onError: () => { onError: () => {
toast.custom((t) => <Toast type="error" body="Client could not be added" t={t}/>); toast.custom((t) => <Toast type="error" body="Client could not be added" t={t}/>);
} }
} });
);
const testClientMutation = useMutation( const onSubmit = (data: unknown) => addMutation.mutate(data as DownloadClient);
(client: DownloadClient) => APIClient.download_clients.test(client),
{ const testClientMutation = useMutation({
mutationFn: (client: DownloadClient) => APIClient.download_clients.test(client),
onMutate: () => { onMutate: () => {
setIsTesting(true); setIsTesting(true);
setIsErrorTest(false); setIsErrorTest(false);
@ -560,16 +559,9 @@ export function DownloadClientAddForm({ isOpen, toggle }: formProps) {
setIsErrorTest(false); setIsErrorTest(false);
}); });
} }
} });
);
const onSubmit = (data: unknown) => { const testClient = (data: unknown) => testClientMutation.mutate(data as DownloadClient);
mutation.mutate(data as DownloadClient);
};
const testClient = (data: unknown) => {
testClientMutation.mutate(data as DownloadClient);
};
const initialValues: InitialValues = { const initialValues: InitialValues = {
name: "", name: "",
@ -692,33 +684,40 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: updateFormP
const [isErrorTest, setIsErrorTest] = useState(false); const [isErrorTest, setIsErrorTest] = useState(false);
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false); const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
const cancelButtonRef = useRef(null);
const cancelModalButtonRef = useRef(null);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const mutation = useMutation( const mutation = useMutation({
(client: DownloadClient) => APIClient.download_clients.update(client), mutationFn: (client: DownloadClient) => APIClient.download_clients.update(client),
{
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["downloadClients"]); queryClient.invalidateQueries({ queryKey: clientKeys.lists() });
queryClient.invalidateQueries({ queryKey: clientKeys.detail(client.id) });
toast.custom((t) => <Toast type="success" body={`${client.name} was updated successfully`} t={t}/>); toast.custom((t) => <Toast type="success" body={`${client.name} was updated successfully`} t={t}/>);
toggle(); toggle();
} }
} });
);
const deleteMutation = useMutation( const onSubmit = (data: unknown) => mutation.mutate(data as DownloadClient);
(clientID: number) => APIClient.download_clients.delete(clientID),
{ const deleteMutation = useMutation({
mutationFn: (clientID: number) => APIClient.download_clients.delete(clientID),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(); queryClient.invalidateQueries({ queryKey: clientKeys.lists() });
queryClient.invalidateQueries({ queryKey: clientKeys.detail(client.id) });
toast.custom((t) => <Toast type="success" body={`${client.name} was deleted.`} t={t}/>); toast.custom((t) => <Toast type="success" body={`${client.name} was deleted.`} t={t}/>);
toggleDeleteModal(); toggleDeleteModal();
} }
} });
);
const testClientMutation = useMutation( const deleteAction = () => deleteMutation.mutate(client.id);
(client: DownloadClient) => APIClient.download_clients.test(client),
{
const testClientMutation = useMutation({
mutationFn: (client: DownloadClient) => APIClient.download_clients.test(client),
onMutate: () => { onMutate: () => {
setIsTesting(true); setIsTesting(true);
setIsErrorTest(false); setIsErrorTest(false);
@ -743,23 +742,9 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: updateFormP
setIsErrorTest(false); setIsErrorTest(false);
}); });
} }
} });
);
const onSubmit = (data: unknown) => { const testClient = (data: unknown) => testClientMutation.mutate(data as DownloadClient);
mutation.mutate(data as DownloadClient);
};
const cancelButtonRef = useRef(null);
const cancelModalButtonRef = useRef(null);
const deleteAction = () => {
deleteMutation.mutate(client.id);
};
const testClient = (data: unknown) => {
testClientMutation.mutate(data as DownloadClient);
};
const initialValues = { const initialValues = {
id: client.id, id: client.id,

View file

@ -1,16 +1,18 @@
import { useMutation, useQueryClient } from "react-query";
import { APIClient } from "../../api/APIClient";
import { toast } from "react-hot-toast";
import Toast from "../../components/notifications/Toast";
import { SlideOver } from "../../components/panels";
import { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "../../components/inputs";
import { SelectFieldBasic } from "../../components/inputs/select_wide";
import { componentMapType } from "./DownloadClientForms";
import { sleep } from "../../utils";
import { useState } from "react"; import { useState } from "react";
import { ImplementationBadges } from "../../screens/settings/Indexer"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "react-hot-toast";
import { useFormikContext } from "formik"; import { useFormikContext } from "formik";
import { FeedDownloadTypeOptions } from "../../domain/constants";
import { APIClient } from "@api/APIClient";
import Toast from "@components/notifications/Toast";
import { SlideOver } from "@components/panels";
import { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "@components/inputs";
import { SelectFieldBasic } from "@components/inputs/select_wide";
import { componentMapType } from "./DownloadClientForms";
import { sleep } from "@utils";
import { ImplementationBadges } from "@screens/settings/Indexer";
import { FeedDownloadTypeOptions } from "@domain/constants";
import { feedKeys } from "@screens/settings/Feed";
interface UpdateProps { interface UpdateProps {
isOpen: boolean; isOpen: boolean;
@ -40,38 +42,31 @@ export function FeedUpdateForm({ isOpen, toggle, feed }: UpdateProps) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const mutation = useMutation( const mutation = useMutation({
(feed: Feed) => APIClient.feeds.update(feed), mutationFn: (feed: Feed) => APIClient.feeds.update(feed),
{
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["feeds"]); queryClient.invalidateQueries({ queryKey: feedKeys.lists() });
toast.custom((t) => <Toast type="success" body={`${feed.name} was updated successfully`} t={t} />); toast.custom((t) => <Toast type="success" body={`${feed.name} was updated successfully`} t={t} />);
toggle(); toggle();
} }
} });
);
const onSubmit = (formData: unknown) => { const onSubmit = (formData: unknown) => mutation.mutate(formData as Feed);
mutation.mutate(formData as Feed);
};
const deleteMutation = useMutation( const deleteMutation = useMutation({
(feedID: number) => APIClient.feeds.delete(feedID), mutationFn: (feedID: number) => APIClient.feeds.delete(feedID),
{
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["feeds"]); queryClient.invalidateQueries({ queryKey: feedKeys.lists() });
toast.custom((t) => <Toast type="success" body={`${feed.name} was deleted.`} t={t} />); toast.custom((t) => <Toast type="success" body={`${feed.name} was deleted.`} t={t} />);
} }
} });
);
const deleteAction = () => { const deleteAction = () => deleteMutation.mutate(feed.id);
deleteMutation.mutate(feed.id);
};
const testFeedMutation = useMutation( const testFeedMutation = useMutation({
(feed: Feed) => APIClient.feeds.test(feed), mutationFn: (feed: Feed) => APIClient.feeds.test(feed),
{
onMutate: () => { onMutate: () => {
setIsTesting(true); setIsTesting(true);
setIsErrorTest(false); setIsErrorTest(false);
@ -96,12 +91,9 @@ export function FeedUpdateForm({ isOpen, toggle, feed }: UpdateProps) {
setIsErrorTest(false); setIsErrorTest(false);
}); });
} }
} });
);
const testFeed = (data: unknown) => { const testFeed = (data: unknown) => testFeedMutation.mutate(data as Feed);
testFeedMutation.mutate(data as Feed);
};
const initialValues: InitialValues = { const initialValues: InitialValues = {
id: feed.id, id: feed.id,

View file

@ -1,20 +1,23 @@
import React, { Fragment, useState } from "react"; import React, { Fragment, useState } from "react";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { useMutation, useQuery, useQueryClient } from "react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select"; import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select";
import type { FieldProps } from "formik"; import type { FieldProps } from "formik";
import { Field, Form, Formik, FormikValues } from "formik"; import { Field, Form, Formik, FormikValues } from "formik";
import { XMarkIcon } from "@heroicons/react/24/solid"; import { XMarkIcon } from "@heroicons/react/24/solid";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { classNames, sleep } from "../../utils";
import DEBUG from "../../components/debug"; import { classNames, sleep } from "@utils";
import { APIClient } from "../../api/APIClient"; import DEBUG from "@components/debug";
import { PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "../../components/inputs"; import { APIClient } from "@api/APIClient";
import { SlideOver } from "../../components/panels"; import { PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "@components/inputs";
import Toast from "../../components/notifications/Toast"; import { SlideOver } from "@components/panels";
import { SelectFieldBasic, SelectFieldCreatable } from "../../components/inputs/select_wide"; import Toast from "@components/notifications/Toast";
import { CustomTooltip } from "../../components/tooltips/CustomTooltip"; import { SelectFieldBasic, SelectFieldCreatable } from "@components/inputs/select_wide";
import { FeedDownloadTypeOptions } from "../../domain/constants"; import { CustomTooltip } from "@components/tooltips/CustomTooltip";
import { FeedDownloadTypeOptions } from "@domain/constants";
import { feedKeys } from "@screens/settings/Feed";
import { indexerKeys } from "@screens/settings/Indexer";
const Input = (props: InputProps) => ( const Input = (props: InputProps) => (
<components.Input <components.Input
@ -244,19 +247,18 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
const [indexer, setIndexer] = useState<IndexerDefinition>({} as IndexerDefinition); const [indexer, setIndexer] = useState<IndexerDefinition>({} as IndexerDefinition);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { data } = useQuery( const { data } = useQuery({
"indexerDefinition", queryKey: ["indexerDefinition"],
() => APIClient.indexers.getSchema(), queryFn: APIClient.indexers.getSchema,
{
enabled: isOpen, enabled: isOpen,
refetchOnWindowFocus: false refetchOnWindowFocus: false
} });
);
const mutation = useMutation( const mutation = useMutation({
(indexer: Indexer) => APIClient.indexers.create(indexer), { mutationFn: (indexer: Indexer) => APIClient.indexers.create(indexer),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["indexer"]); queryClient.invalidateQueries({ queryKey: indexerKeys.lists() });
toast.custom((t) => <Toast type="success" body="Indexer was added" t={t} />); toast.custom((t) => <Toast type="success" body="Indexer was added" t={t} />);
sleep(1500); sleep(1500);
toggle(); toggle();
@ -266,13 +268,16 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
} }
}); });
const ircMutation = useMutation( const ircMutation = useMutation({
(network: IrcNetworkCreate) => APIClient.irc.createNetwork(network) mutationFn: (network: IrcNetworkCreate) => APIClient.irc.createNetwork(network)
); });
const feedMutation = useMutation( const feedMutation = useMutation({
(feed: FeedCreate) => APIClient.feeds.create(feed) mutationFn: (feed: FeedCreate) => APIClient.feeds.create(feed),
); onSuccess: () => {
queryClient.invalidateQueries({ queryKey: feedKeys.lists() });
}
});
const onSubmit = (formData: FormikValues) => { const onSubmit = (formData: FormikValues) => {
const ind = data && data.find(i => i.identifier === formData.identifier); const ind = data && data.find(i => i.identifier === formData.identifier);
@ -587,9 +592,8 @@ function TestApiButton({ values, show }: TestApiButtonProps) {
return null; return null;
} }
const testApiMutation = useMutation( const testApiMutation = useMutation({
(req: IndexerTestApiReq) => APIClient.indexers.testApi(req), mutationFn: (req: IndexerTestApiReq) => APIClient.indexers.testApi(req),
{
onMutate: () => { onMutate: () => {
setIsTesting(true); setIsTesting(true);
setIsErrorTest(false); setIsErrorTest(false);
@ -618,8 +622,7 @@ function TestApiButton({ values, show }: TestApiButtonProps) {
setIsErrorTest(false); setIsErrorTest(false);
}); });
} }
} });
);
const testApi = () => { const testApi = () => {
const req: IndexerTestApiReq = { const req: IndexerTestApiReq = {
@ -706,9 +709,11 @@ interface UpdateProps {
export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) { export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const mutation = useMutation((indexer: Indexer) => APIClient.indexers.update(indexer), { const mutation = useMutation({
mutationFn: (indexer: Indexer) => APIClient.indexers.update(indexer),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["indexer"]); queryClient.invalidateQueries({ queryKey: indexerKeys.lists() });
toast.custom((t) => <Toast type="success" body={`${indexer.name} was updated successfully`} t={t} />); toast.custom((t) => <Toast type="success" body={`${indexer.name} was updated successfully`} t={t} />);
sleep(1500); sleep(1500);
@ -716,23 +721,23 @@ export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) {
} }
}); });
const deleteMutation = useMutation((id: number) => APIClient.indexers.delete(id), {
onSuccess: () => {
queryClient.invalidateQueries(["indexer"]);
toast.custom((t) => <Toast type="success" body={`${indexer.name} was deleted.`} t={t} />);
toggle();
}
});
const onSubmit = (data: unknown) => { const onSubmit = (data: unknown) => {
// TODO clear data depending on type // TODO clear data depending on type
mutation.mutate(data as Indexer); mutation.mutate(data as Indexer);
}; };
const deleteAction = () => { const deleteMutation = useMutation({
deleteMutation.mutate(indexer.id ?? 0); mutationFn: (id: number) => APIClient.indexers.delete(id),
}; onSuccess: () => {
queryClient.invalidateQueries({ queryKey: indexerKeys.lists() });
toast.custom((t) => <Toast type="success" body={`${indexer.name} was deleted.`} t={t} />);
toggle();
}
});
const deleteAction = () => deleteMutation.mutate(indexer.id ?? 0);
const renderSettingFields = (settings: IndexerSetting[]) => { const renderSettingFields = (settings: IndexerSetting[]) => {
if (settings === undefined) { if (settings === undefined) {

View file

@ -1,13 +1,18 @@
import { useMutation, useQueryClient } from "react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { XMarkIcon } from "@heroicons/react/24/solid"; import { XMarkIcon } from "@heroicons/react/24/solid";
import type { FieldProps } from "formik"; import type { FieldProps } from "formik";
import { Field, FieldArray, FormikErrors, FormikValues } from "formik"; import { Field, FieldArray, FormikErrors, FormikValues } from "formik";
import { APIClient } from "../../api/APIClient";
import { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, SwitchGroupWideRed, TextFieldWide } from "../../components/inputs";
import { SlideOver } from "../../components/panels";
import Toast from "../../components/notifications/Toast";
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select";
import { Dialog } from "@headlessui/react";
import { IrcAuthMechanismTypeOptions, OptionBasicTyped } from "@domain/constants";
import { ircKeys } from "@screens/settings/Irc";
import { APIClient } from "@api/APIClient";
import { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, SwitchGroupWideRed, TextFieldWide } from "@components/inputs";
import { SlideOver } from "@components/panels";
import Toast from "@components/notifications/Toast";
interface ChannelsFieldArrayProps { interface ChannelsFieldArrayProps {
channels: IrcChannel[]; channels: IrcChannel[];
@ -96,43 +101,21 @@ interface AddFormProps {
export function IrcNetworkAddForm({ isOpen, toggle }: AddFormProps) { export function IrcNetworkAddForm({ isOpen, toggle }: AddFormProps) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const mutation = useMutation(
(network: IrcNetwork) => APIClient.irc.createNetwork(network), const mutation = useMutation({
{ mutationFn: (network: IrcNetwork) => APIClient.irc.createNetwork(network),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["networks"]); queryClient.invalidateQueries({ queryKey: ircKeys.lists() });
toast.custom((t) => <Toast type="success" body="IRC Network added. Please allow up to 30 seconds for the network to come online." t={t} />); toast.custom((t) => <Toast type="success" body="IRC Network added. Please allow up to 30 seconds for the network to come online." t={t} />);
toggle(); toggle();
}, },
onError: () => { onError: () => {
toast.custom((t) => <Toast type="error" body="IRC Network could not be added" t={t} />); toast.custom((t) => <Toast type="error" body="IRC Network could not be added" t={t} />);
} }
} });
);
const onSubmit = (data: unknown) => { const onSubmit = (data: unknown) => mutation.mutate(data as IrcNetwork);
mutation.mutate(data as IrcNetwork);
};
const validate = (values: FormikValues) => {
const errors = {} as FormikErrors<FormikValues>;
if (!values.name)
errors.name = "Required";
if (!values.port)
errors.port = "Required";
if (!values.server)
errors.server = "Required";
if (!values.nick)
errors.nick = "Required";
// if (!values.auth || !values.auth.account)
// errors.auth = { account: "Required" };
return errors;
};
const initialValues: IrcNetworkAddFormValues = { const initialValues: IrcNetworkAddFormValues = {
name: "", name: "",
@ -157,7 +140,7 @@ export function IrcNetworkAddForm({ isOpen, toggle }: AddFormProps) {
toggle={toggle} toggle={toggle}
onSubmit={onSubmit} onSubmit={onSubmit}
initialValues={initialValues} initialValues={initialValues}
validate={validate} validate={validateNetwork}
> >
{(values) => ( {(values) => (
<div className="flex flex-col space-y-4 px-1 py-6 sm:py-0 sm:space-y-0"> <div className="flex flex-col space-y-4 px-1 py-6 sm:py-0 sm:space-y-0">
@ -214,6 +197,28 @@ export function IrcNetworkAddForm({ isOpen, toggle }: AddFormProps) {
); );
} }
const validateNetwork = (values: FormikValues) => {
const errors = {} as FormikErrors<FormikValues>;
if (!values.name) {
errors.name = "Required";
}
if (!values.server) {
errors.server = "Required";
}
if (!values.port) {
errors.port = "Required";
}
if (!values.nick) {
errors.nick = "Required";
}
return errors;
};
interface IrcNetworkUpdateFormValues { interface IrcNetworkUpdateFormValues {
id: number; id: number;
name: string; name: string;
@ -241,53 +246,31 @@ export function IrcNetworkUpdateForm({
}: IrcNetworkUpdateFormProps) { }: IrcNetworkUpdateFormProps) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const mutation = useMutation((network: IrcNetwork) => APIClient.irc.updateNetwork(network), { const updateMutation = useMutation({
mutationFn: (network: IrcNetwork) => APIClient.irc.updateNetwork(network),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["networks"]); queryClient.invalidateQueries({ queryKey: ircKeys.lists() });
toast.custom((t) => <Toast type="success" body={`${network.name} was updated successfully`} t={t} />); toast.custom((t) => <Toast type="success" body={`${network.name} was updated successfully`} t={t} />);
toggle(); toggle();
} }
}); });
const deleteMutation = useMutation((id: number) => APIClient.irc.deleteNetwork(id), { const onSubmit = (data: unknown) => updateMutation.mutate(data as IrcNetwork);
const deleteMutation = useMutation({
mutationFn: (id: number) => APIClient.irc.deleteNetwork(id),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["networks"]); queryClient.invalidateQueries({ queryKey: ircKeys.lists() });
toast.custom((t) => <Toast type="success" body={`${network.name} was deleted.`} t={t} />); toast.custom((t) => <Toast type="success" body={`${network.name} was deleted.`} t={t} />);
toggle(); toggle();
} }
}); });
const onSubmit = (data: unknown) => { const deleteAction = () => deleteMutation.mutate(network.id);
console.log("submit: ", data);
mutation.mutate(data as IrcNetwork);
};
const validate = (values: FormikValues) => {
const errors = {} as FormikErrors<FormikValues>;
if (!values.name) {
errors.name = "Required";
}
if (!values.server) {
errors.server = "Required";
}
if (!values.port) {
errors.port = "Required";
}
if (!values.nick) {
errors.nick = "Required";
}
return errors;
};
const deleteAction = () => {
deleteMutation.mutate(network.id);
};
const initialValues: IrcNetworkUpdateFormValues = { const initialValues: IrcNetworkUpdateFormValues = {
id: network.id, id: network.id,
@ -312,7 +295,7 @@ export function IrcNetworkUpdateForm({
onSubmit={onSubmit} onSubmit={onSubmit}
deleteAction={deleteAction} deleteAction={deleteAction}
initialValues={initialValues} initialValues={initialValues}
validate={validate} validate={validateNetwork}
> >
{(values) => ( {(values) => (
<div className="flex flex-col space-y-4 px-1 py-6 sm:py-0 sm:space-y-0"> <div className="flex flex-col space-y-4 px-1 py-6 sm:py-0 sm:space-y-0">
@ -459,10 +442,6 @@ function SelectField<T>({ name, label, options }: SelectFieldProps<T>) {
); );
} }
import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select";
import { IrcAuthMechanismTypeOptions, OptionBasicTyped } from "../../domain/constants";
import { Dialog } from "@headlessui/react";
const Input = (props: InputProps) => { const Input = (props: InputProps) => {
return ( return (
<components.Input <components.Input

View file

@ -4,15 +4,17 @@ import type { FieldProps } from "formik";
import { Field, Form, Formik, FormikErrors, FormikValues } from "formik"; import { Field, Form, Formik, FormikErrors, FormikValues } from "formik";
import { XMarkIcon } from "@heroicons/react/24/solid"; import { XMarkIcon } from "@heroicons/react/24/solid";
import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select"; import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select";
import { PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "../../components/inputs"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import DEBUG from "../../components/debug";
import { EventOptions, NotificationTypeOptions, SelectOption } from "../../domain/constants";
import { useMutation, useQueryClient } from "react-query";
import { APIClient } from "../../api/APIClient";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import Toast from "../../components/notifications/Toast";
import { SlideOver } from "../../components/panels"; import { PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "@components/inputs";
import DEBUG from "@components/debug";
import { EventOptions, NotificationTypeOptions, SelectOption } from "@domain/constants";
import { APIClient } from "@api/APIClient";
import Toast from "@components/notifications/Toast";
import { SlideOver } from "@components/panels";
import { componentMapType } from "./DownloadClientForms"; import { componentMapType } from "./DownloadClientForms";
import { notificationKeys } from "@screens/settings/Notifications";
const Input = (props: InputProps) => { const Input = (props: InputProps) => {
return ( return (
@ -137,36 +139,29 @@ interface AddProps {
export function NotificationAddForm({ isOpen, toggle }: AddProps) { export function NotificationAddForm({ isOpen, toggle }: AddProps) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const mutation = useMutation( const createMutation = useMutation({
(notification: Notification) => APIClient.notifications.create(notification), mutationFn: (notification: Notification) => APIClient.notifications.create(notification),
{
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["notifications"]); queryClient.invalidateQueries({ queryKey: notificationKeys.lists() });
toast.custom((t) => <Toast type="success" body="Notification added!" t={t} />); toast.custom((t) => <Toast type="success" body="Notification added!" t={t} />);
toggle(); toggle();
}, },
onError: () => { onError: () => {
toast.custom((t) => <Toast type="error" body="Notification could not be added" t={t} />); toast.custom((t) => <Toast type="error" body="Notification could not be added" t={t} />);
} }
} });
);
const onSubmit = (formData: unknown) => { const onSubmit = (formData: unknown) => createMutation.mutate(formData as Notification);
mutation.mutate(formData as Notification);
};
const testMutation = useMutation( const testMutation = useMutation({
(n: Notification) => APIClient.notifications.test(n), mutationFn: (n: Notification) => APIClient.notifications.test(n),
{
onError: (err) => { onError: (err) => {
console.error(err); console.error(err);
} }
} });
);
const testNotification = (data: unknown) => { const testNotification = (data: unknown) => testMutation.mutate(data as Notification);
testMutation.mutate(data as Notification);
};
const validate = (values: NotificationAddFormValues) => { const validate = (values: NotificationAddFormValues) => {
const errors = {} as FormikErrors<FormikValues>; const errors = {} as FormikErrors<FormikValues>;
@ -410,47 +405,37 @@ interface InitialValues {
export function NotificationUpdateForm({ isOpen, toggle, notification }: UpdateProps) { export function NotificationUpdateForm({ isOpen, toggle, notification }: UpdateProps) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const mutation = useMutation( const mutation = useMutation({
(notification: Notification) => APIClient.notifications.update(notification), mutationFn: (notification: Notification) => APIClient.notifications.update(notification),
{
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["notifications"]); queryClient.invalidateQueries({ queryKey: notificationKeys.lists() });
toast.custom((t) => <Toast type="success" body={`${notification.name} was updated successfully`} t={t}/>); toast.custom((t) => <Toast type="success" body={`${notification.name} was updated successfully`} t={t}/>);
toggle(); toggle();
} }
} });
);
const deleteMutation = useMutation( const onSubmit = (formData: unknown) => mutation.mutate(formData as Notification);
(notificationID: number) => APIClient.notifications.delete(notificationID),
{ const deleteMutation = useMutation({
mutationFn: (notificationID: number) => APIClient.notifications.delete(notificationID),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["notifications"]); queryClient.invalidateQueries({ queryKey: notificationKeys.lists() });
toast.custom((t) => <Toast type="success" body={`${notification.name} was deleted.`} t={t}/>); toast.custom((t) => <Toast type="success" body={`${notification.name} was deleted.`} t={t}/>);
} }
} });
);
const onSubmit = (formData: unknown) => { const deleteAction = () => deleteMutation.mutate(notification.id);
mutation.mutate(formData as Notification);
};
const deleteAction = () => { const testMutation = useMutation({
deleteMutation.mutate(notification.id); mutationFn: (n: Notification) => APIClient.notifications.test(n),
};
const testMutation = useMutation(
(n: Notification) => APIClient.notifications.test(n),
{
onError: (err) => { onError: (err) => {
console.error(err); console.error(err);
} }
} });
);
const testNotification = (data: unknown) => { const testNotification = (data: unknown) => testMutation.mutate(data as Notification);
testMutation.mutate(data as Notification);
};
const initialValues: InitialValues = { const initialValues: InitialValues = {
id: notification.id, id: notification.id,

View file

@ -1,26 +1,22 @@
import { Fragment } from "react"; import React, { Fragment } from "react";
import { Link, NavLink, Outlet } from "react-router-dom"; import { Link, NavLink, Outlet } from "react-router-dom";
import { Disclosure, Menu, Transition } from "@headlessui/react"; import { Disclosure, Menu, Transition } from "@headlessui/react";
import { ArrowTopRightOnSquareIcon, UserIcon } from "@heroicons/react/24/solid"; import { ArrowTopRightOnSquareIcon, UserIcon } from "@heroicons/react/24/solid";
import { Bars3Icon, XMarkIcon, MegaphoneIcon } from "@heroicons/react/24/outline"; import { Bars3Icon, XMarkIcon, MegaphoneIcon } from "@heroicons/react/24/outline";
import { useMutation, useQuery } from "@tanstack/react-query";
import { AuthContext } from "../utils/Context";
import logo from "../logo.png";
import { useQuery } from "react-query";
import { APIClient } from "../api/APIClient";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import Toast from "@/components/notifications/Toast";
import { AuthContext } from "@utils/Context";
import logo from "@app/logo.png";
import { APIClient } from "@api/APIClient";
import Toast from "@components/notifications/Toast";
import { classNames } from "@utils";
interface NavItem { interface NavItem {
name: string; name: string;
path: string; path: string;
} }
function classNames(...classes: string[]) {
return classes.filter(Boolean).join(" ");
}
const nav: Array<NavItem> = [ const nav: Array<NavItem> = [
{ name: "Dashboard", path: "/" }, { name: "Dashboard", path: "/" },
{ name: "Filters", path: "/filters" }, { name: "Filters", path: "/filters" },
@ -32,24 +28,27 @@ const nav: Array<NavItem> = [
export default function Base() { export default function Base() {
const authContext = AuthContext.useValue(); const authContext = AuthContext.useValue();
const { data } = useQuery( const { data } = useQuery({
["updates"], queryKey: ["updates"],
() => APIClient.updates.getLatestRelease(), queryFn: () => APIClient.updates.getLatestRelease(),
{
retry: false, retry: false,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
onError: err => console.log(err) onError: err => console.log(err)
} });
);
const LogOutUser = () => { const logoutMutation = useMutation( {
APIClient.auth.logout() mutationFn: APIClient.auth.logout,
.then(() => { onSuccess: () => {
AuthContext.reset(); AuthContext.reset();
toast.custom((t) => ( toast.custom((t) => (
<Toast type="success" body="You have been logged out. Goodbye!" t={t} /> <Toast type="success" body="You have been logged out. Goodbye!" t={t} />
)); ));
}
}); });
const logoutAction = () => {
logoutMutation.mutate();
}; };
return ( return (
@ -172,7 +171,7 @@ export default function Base() {
<Menu.Item> <Menu.Item>
{({ active }) => ( {({ active }) => (
<button <button
onClick={LogOutUser} onClick={logoutAction}
className={classNames( className={classNames(
active active
? "bg-gray-100 dark:bg-gray-600" ? "bg-gray-100 dark:bg-gray-600"
@ -242,7 +241,7 @@ export default function Base() {
</NavLink> </NavLink>
))} ))}
<button <button
onClick={LogOutUser} onClick={logoutAction}
className="w-full shadow-sm border bg-gray-100 border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-white block px-3 py-2 rounded-md text-base font-medium text-left" className="w-full shadow-sm border bg-gray-100 border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-white block px-3 py-2 rounded-md text-base font-medium text-left"
> >
Logout Logout

View file

@ -2,20 +2,20 @@ import { Fragment, useEffect, useRef, useState } from "react";
import { ExclamationTriangleIcon } from "@heroicons/react/24/solid"; import { ExclamationTriangleIcon } from "@heroicons/react/24/solid";
import format from "date-fns/format"; import format from "date-fns/format";
import { DebounceInput } from "react-debounce-input"; import { DebounceInput } from "react-debounce-input";
import { APIClient } from "../api/APIClient";
import { Checkbox } from "../components/Checkbox";
import { classNames, simplifyDate } from "../utils";
import { SettingsContext } from "../utils/Context";
import { EmptySimple } from "../components/emptystates";
import { import {
Cog6ToothIcon, Cog6ToothIcon,
DocumentArrowDownIcon DocumentArrowDownIcon
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { useQuery } from "react-query"; import { useQuery } from "@tanstack/react-query";
import { Menu, Transition } from "@headlessui/react"; import { Menu, Transition } from "@headlessui/react";
import { baseUrl } from "../utils";
import { RingResizeSpinner } from "@/components/Icons";
import { APIClient } from "@api/APIClient";
import { Checkbox } from "@components/Checkbox";
import { classNames, simplifyDate } from "@utils";
import { SettingsContext } from "@utils/Context";
import { EmptySimple } from "@components/emptystates";
import { baseUrl } from "@utils";
import { RingResizeSpinner } from "@components/Icons";
type LogEvent = { type LogEvent = {
time: string; time: string;
@ -83,7 +83,6 @@ export const Logs = () => {
</div> </div>
</header> </header>
<div className="max-w-screen-xl mx-auto pb-12 px-2 sm:px-4 lg:px-8"> <div className="max-w-screen-xl mx-auto pb-12 px-2 sm:px-4 lg:px-8">
<div className="flex justify-center py-4"> <div className="flex justify-center py-4">
<ExclamationTriangleIcon <ExclamationTriangleIcon
@ -158,15 +157,13 @@ export const Logs = () => {
}; };
export const LogFiles = () => { export const LogFiles = () => {
const { isLoading, data } = useQuery( const { isLoading, data } = useQuery({
["log-files"], queryKey: ["log-files"],
() => APIClient.logs.files(), queryFn: () => APIClient.logs.files(),
{
retry: false, retry: false,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
onError: err => console.log(err) onError: err => console.log(err)
} });
);
return ( return (
<div> <div>
@ -225,9 +222,7 @@ const LogFilesItem = ({ file }: LogFilesItemProps) => {
setIsDownloading(false); setIsDownloading(false);
}; };
return ( return (
<li className="text-gray-500 dark:text-gray-400"> <li className="text-gray-500 dark:text-gray-400">
<div className="grid grid-cols-12 items-center py-2"> <div className="grid grid-cols-12 items-center py-2">
<div className="col-span-4 sm:col-span-5 px-2 py-0 truncate hidden sm:block sm:text-sm text-md font-medium text-gray-900 dark:text-gray-200"> <div className="col-span-4 sm:col-span-5 px-2 py-0 truncate hidden sm:block sm:text-sm text-md font-medium text-gray-900 dark:text-gray-200">

View file

@ -10,7 +10,7 @@ import {
Square3Stack3DIcon Square3Stack3DIcon
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { classNames } from "../utils"; import { classNames } from "@utils";
interface NavTabType { interface NavTabType {
name: string; name: string;

View file

@ -1,14 +1,15 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useMutation } from "react-query"; import { useMutation } from "@tanstack/react-query";
import logo from "../../logo.png";
import { APIClient } from "../../api/APIClient";
import { AuthContext } from "../../utils/Context";
import { PasswordInput, TextInput } from "../../components/inputs/text";
import { Tooltip } from "react-tooltip";
import Toast from "@/components/notifications/Toast";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { Tooltip } from "react-tooltip";
import logo from "@app/logo.png";
import { APIClient } from "@api/APIClient";
import { AuthContext } from "@utils/Context";
import { PasswordInput, TextInput } from "@components/inputs/text";
import Toast from "@components/notifications/Toast";
type LoginFormFields = { type LoginFormFields = {
username: string; username: string;
@ -37,9 +38,8 @@ export const Login = () => {
.catch(() => { /*don't log to console PAHLLEEEASSSE*/ }); .catch(() => { /*don't log to console PAHLLEEEASSSE*/ });
}, []); }, []);
const loginMutation = useMutation( const loginMutation = useMutation({
(data: LoginFormFields) => APIClient.auth.login(data.username, data.password), mutationFn: (data: LoginFormFields) => APIClient.auth.login(data.username, data.password),
{
onSuccess: (_, variables: LoginFormFields) => { onSuccess: (_, variables: LoginFormFields) => {
setAuthContext({ setAuthContext({
username: variables.username, username: variables.username,
@ -52,8 +52,7 @@ export const Login = () => {
<Toast type="error" body="Wrong password or username!" t={t} /> <Toast type="error" body="Wrong password or username!" t={t} />
)); ));
} }
} });
);
const onSubmit = (data: LoginFormFields) => loginMutation.mutate(data); const onSubmit = (data: LoginFormFields) => loginMutation.mutate(data);

View file

@ -1,10 +1,10 @@
import { Form, Formik } from "formik"; import { Form, Formik } from "formik";
import { useMutation } from "react-query"; import { useMutation } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { APIClient } from "../../api/APIClient";
import { TextField, PasswordField } from "../../components/inputs"; import { APIClient } from "@api/APIClient";
import logo from "../../logo.png"; import { TextField, PasswordField } from "@components/inputs";
import logo from "@app/logo.png";
interface InputValues { interface InputValues {
username: string; username: string;
@ -33,10 +33,10 @@ export const Onboarding = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const mutation = useMutation( const mutation = useMutation({
(data: InputValues) => APIClient.auth.onboard(data.username, data.password1), mutationFn: (data: InputValues) => APIClient.auth.onboard(data.username, data.password1),
{ onSuccess: () => navigate("/") } onSuccess: () => navigate("/")
); });
return ( return (
<div className="min-h-screen flex flex-col justify-center py-12 sm:px-6 lg:px-8"> <div className="min-h-screen flex flex-col justify-center py-12 sm:px-6 lg:px-8">

View file

@ -1,5 +1,5 @@
import * as React from "react"; import * as React from "react";
import { useQuery } from "react-query"; import { useQuery } from "@tanstack/react-query";
import { import {
useTable, useTable,
useFilters, useFilters,
@ -8,11 +8,10 @@ import {
usePagination, FilterProps, Column usePagination, FilterProps, Column
} from "react-table"; } from "react-table";
import { APIClient } from "../../api/APIClient"; import { APIClient } from "@api/APIClient";
import { EmptyListState } from "../../components/emptystates"; import { EmptyListState } from "@components/emptystates";
import * as Icons from "@components/Icons";
import * as Icons from "../../components/Icons"; import * as DataTable from "@components/data-table";
import * as DataTable from "../../components/data-table";
// This is a custom filter UI for selecting // This is a custom filter UI for selecting
// a unique option from a list // a unique option from a list
@ -74,8 +73,9 @@ function Table({ columns, data }: TableProps) {
usePagination usePagination
); );
if (!page.length) if (!page.length) {
return <EmptyListState text="No recent activity" />; return <EmptyListState text="No recent activity" />;
}
// Render the UI for your table // Render the UI for your table
return ( return (
@ -178,13 +178,13 @@ export const ActivityTable = () => {
} }
], []); ], []);
const { isLoading, data } = useQuery( const { isLoading, data } = useQuery({
"dash_recent_releases", queryKey: ["dash_recent_releases"],
() => APIClient.release.findRecent(), queryFn: APIClient.release.findRecent,
{ refetchOnWindowFocus: false } refetchOnWindowFocus: false
); });
if (isLoading) if (isLoading) {
return ( return (
<div className="flex flex-col mt-12"> <div className="flex flex-col mt-12">
<h3 className="text-2xl font-medium leading-6 text-gray-900 dark:text-gray-200"> <h3 className="text-2xl font-medium leading-6 text-gray-900 dark:text-gray-200">
@ -195,6 +195,7 @@ export const ActivityTable = () => {
</div> </div>
</div> </div>
); );
}
return ( return (
<div className="flex flex-col mt-12"> <div className="flex flex-col mt-12">

View file

@ -1,6 +1,6 @@
import { useQuery } from "react-query"; import { useQuery } from "@tanstack/react-query";
import { APIClient } from "../../api/APIClient"; import { APIClient } from "@api/APIClient";
import { classNames } from "../../utils"; import { classNames } from "@utils";
interface StatsItemProps { interface StatsItemProps {
name: string; name: string;
@ -28,17 +28,18 @@ const StatsItem = ({ name, placeholder, value }: StatsItemProps) => (
); );
export const Stats = () => { export const Stats = () => {
const { isLoading, data } = useQuery( const { isLoading, data } = useQuery({
"dash_release_stats", queryKey: ["dash_release_stats"],
() => APIClient.release.stats(), queryFn: APIClient.release.stats,
{ refetchOnWindowFocus: false } refetchOnWindowFocus: false
); });
return ( return (
<div> <div>
<h1 className="text-3xl font-bold text-black dark:text-white"> <h1 className="text-3xl font-bold text-black dark:text-white">
Stats Stats
</h1> </h1>
<dl className={classNames("grid grid-cols-1 gap-5 mt-5 sm:grid-cols-2 lg:grid-cols-3", isLoading ? "animate-pulse" : "")}> <dl className={classNames("grid grid-cols-1 gap-5 mt-5 sm:grid-cols-2 lg:grid-cols-3", isLoading ? "animate-pulse" : "")}>
<StatsItem name="Filtered Releases" value={data?.filtered_count ?? 0} /> <StatsItem name="Filtered Releases" value={data?.filtered_count ?? 0} />
{/* <StatsItem name="Filter Rejected Releases" stat={data?.filter_rejected_count} /> */} {/* <StatsItem name="Filter Rejected Releases" stat={data?.filter_rejected_count} /> */}

View file

@ -1,21 +1,21 @@
import { AlertWarning } from "../../components/alerts"; import { AlertWarning } from "@components/alerts";
import { DownloadClientSelect, NumberField, Select, SwitchGroup, TextField } from "../../components/inputs"; import { DownloadClientSelect, NumberField, Select, SwitchGroup, TextField } from "@components/inputs";
import { ActionContentLayoutOptions, ActionRtorrentRenameOptions, ActionTypeNameMap, ActionTypeOptions } from "../../domain/constants"; import { ActionContentLayoutOptions, ActionRtorrentRenameOptions, ActionTypeNameMap, ActionTypeOptions } from "@domain/constants";
import React, { Fragment, useRef, useEffect, useState } from "react"; import React, { Fragment, useRef, useEffect, useState } from "react";
import { useQuery } from "react-query"; import { useQuery } from "@tanstack/react-query";
import { APIClient } from "../../api/APIClient"; import { APIClient } from "@api/APIClient";
import { Field, FieldArray, FieldProps, FormikValues } from "formik"; import { Field, FieldArray, FieldProps, FormikValues } from "formik";
import { EmptyListState } from "../../components/emptystates"; import { EmptyListState } from "@components/emptystates";
import { useToggle } from "../../hooks/hooks"; import { useToggle } from "@hooks/hooks";
import { classNames } from "../../utils"; import { classNames } from "@utils";
import { Dialog, Switch as SwitchBasic, Transition } from "@headlessui/react"; import { Dialog, Switch as SwitchBasic, Transition } from "@headlessui/react";
import { ChevronRightIcon } from "@heroicons/react/24/solid"; import { ChevronRightIcon } from "@heroicons/react/24/solid";
import { DeleteModal } from "../../components/modals"; import { DeleteModal } from "@components/modals";
import { CollapsableSection } from "./details"; import { CollapsableSection } from "./details";
import { CustomTooltip } from "../../components/tooltips/CustomTooltip"; import { CustomTooltip } from "@components/tooltips/CustomTooltip";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useFormikContext } from "formik"; import { useFormikContext } from "formik";
import { TextArea } from "../../components/inputs/input"; import { TextArea } from "@components/inputs/input";
interface FilterActionsProps { interface FilterActionsProps {
filter: Filter; filter: Filter;

View file

@ -1,5 +1,5 @@
import React, { useRef } from "react"; import React, { useRef } from "react";
import { useMutation, useQuery, useQueryClient } from "react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { NavLink, Route, Routes, useLocation, useNavigate, useParams } from "react-router-dom"; import { NavLink, Route, Routes, useLocation, useNavigate, useParams } from "react-router-dom";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { Form, Formik, FormikValues, useFormikContext } from "formik"; import { Form, Formik, FormikValues, useFormikContext } from "formik";
@ -20,10 +20,10 @@ import {
SOURCES_MUSIC_OPTIONS, SOURCES_MUSIC_OPTIONS,
SOURCES_OPTIONS, SOURCES_OPTIONS,
tagsMatchLogicOptions tagsMatchLogicOptions
} from "../../domain/constants"; } from "@app/domain/constants";
import { APIClient } from "../../api/APIClient"; import { APIClient } from "@api/APIClient";
import { useToggle } from "../../hooks/hooks"; import { useToggle } from "@hooks/hooks";
import { classNames } from "../../utils"; import { classNames } from "@utils";
import { import {
CheckboxField, CheckboxField,
@ -34,13 +34,14 @@ import {
SwitchGroup, SwitchGroup,
TextField, TextField,
RegexField RegexField
} from "../../components/inputs"; } from "@components/inputs";
import DEBUG from "../../components/debug"; import DEBUG from "@components/debug";
import Toast from "../../components/notifications/Toast"; import Toast from "@components/notifications/Toast";
import { DeleteModal } from "../../components/modals"; import { DeleteModal } from "@components/modals";
import { TitleSubtitle } from "../../components/headings"; import { TitleSubtitle } from "@components/headings";
import { TextArea } from "../../components/inputs/input"; import { TextArea } from "@components/inputs/input";
import { FilterActions } from "./action"; import { FilterActions } from "./action";
import { filterKeys } from "./list";
interface tabType { interface tabType {
name: string; name: string;
@ -144,37 +145,49 @@ export default function FilterDetails() {
const navigate = useNavigate(); const navigate = useNavigate();
const { filterId } = useParams<{ filterId: string }>(); const { filterId } = useParams<{ filterId: string }>();
const { isLoading, data: filter } = useQuery( if (filterId === "0" || undefined) {
["filters", filterId], navigate("/filters");
() => APIClient.filters.getByID(parseInt(filterId ?? "0")), }
{
retry: false, const id = parseInt(filterId!);
const { isLoading, data: filter } = useQuery({
queryKey: filterKeys.detail(id),
queryFn: ({ queryKey }) => APIClient.filters.getByID(queryKey[2]),
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
onError: () => navigate("./") onError: () => {
navigate("/filters");
} }
); });
const updateMutation = useMutation({
mutationFn: (filter: Filter) => APIClient.filters.update(filter),
onSuccess: (newFilter, variables) => {
queryClient.setQueryData(filterKeys.detail(variables.id), newFilter);
queryClient.setQueryData<Filter[]>(filterKeys.lists(), (previous) => {
if (previous) {
return previous.map((filter: Filter) => (filter.id === variables.id ? newFilter : filter));
}
});
const updateMutation = useMutation(
(filter: Filter) => APIClient.filters.update(filter),
{
onSuccess: (_, currentFilter) => {
toast.custom((t) => ( toast.custom((t) => (
<Toast type="success" body={`${currentFilter.name} was updated successfully`} t={t} /> <Toast type="success" body={`${newFilter.name} was updated successfully`} t={t}/>
)); ));
queryClient.refetchQueries(["filters"]);
// queryClient.invalidateQueries(["filters", currentFilter.id]);
} }
} });
);
const deleteMutation = useMutation((id: number) => APIClient.filters.delete(id), { const deleteMutation = useMutation({
mutationFn: (id: number) => APIClient.filters.delete(id),
onSuccess: () => { onSuccess: () => {
// Invalidate filters just in case, most likely not necessary but can't hurt.
queryClient.invalidateQueries({ queryKey: filterKeys.lists() });
queryClient.invalidateQueries({ queryKey: filterKeys.detail(id) });
toast.custom((t) => ( toast.custom((t) => (
<Toast type="success" body={`${filter?.name} was deleted`} t={t} /> <Toast type="success" body={`${filter?.name} was deleted`} t={t} />
)); ));
// Invalidate filters just in case, most likely not necessary but can't hurt.
queryClient.invalidateQueries(["filters"]);
// redirect // redirect
navigate("/filters"); navigate("/filters");
@ -234,7 +247,7 @@ export default function FilterDetails() {
initialValues={{ initialValues={{
id: filter.id, id: filter.id,
name: filter.name, name: filter.name,
enabled: filter.enabled || false, enabled: filter.enabled,
min_size: filter.min_size, min_size: filter.min_size,
max_size: filter.max_size, max_size: filter.max_size,
delay: filter.delay, delay: filter.delay,
@ -298,6 +311,7 @@ export default function FilterDetails() {
external_webhook_expect_status: filter.external_webhook_expect_status || 0 external_webhook_expect_status: filter.external_webhook_expect_status || 0
} as Filter} } as Filter}
onSubmit={handleSubmit} onSubmit={handleSubmit}
enableReinitialize={true}
> >
{({ values, dirty, resetForm }) => ( {({ values, dirty, resetForm }) => (
<Form> <Form>
@ -322,11 +336,11 @@ export default function FilterDetails() {
} }
export function General(){ export function General(){
const { isLoading, data: indexers } = useQuery( const { isLoading, data: indexers } = useQuery({
["filters", "indexer_list"], queryKey: ["filters", "indexer_list"],
() => APIClient.indexers.getOptions(), queryFn: APIClient.indexers.getOptions,
{ refetchOnWindowFocus: false } refetchOnWindowFocus: false
); });
const opts = indexers && indexers.length > 0 ? indexers.map(v => ({ const opts = indexers && indexers.length > 0 ? indexers.map(v => ({
label: v.name, label: v.name,

View file

@ -2,11 +2,10 @@ import { Dispatch, FC, Fragment, MouseEventHandler, useReducer, useRef, useState
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { Listbox, Menu, Switch, Transition } from "@headlessui/react"; import { Listbox, Menu, Switch, Transition } from "@headlessui/react";
import { useMutation, useQuery, useQueryClient } from "react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { FormikValues } from "formik"; import { FormikValues } from "formik";
import { useCallback } from "react"; import { useCallback } from "react";
import { Tooltip } from "react-tooltip"; import { Tooltip } from "react-tooltip";
import { FilterListContext, FilterListState } from "../../utils/Context";
import { import {
ArrowsRightLeftIcon, ArrowsRightLeftIcon,
CheckIcon, CheckIcon,
@ -18,15 +17,25 @@ import {
ChatBubbleBottomCenterTextIcon, ChatBubbleBottomCenterTextIcon,
TrashIcon TrashIcon
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { classNames } from "../../utils";
import { FilterAddForm } from "../../forms";
import { useToggle } from "../../hooks/hooks";
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"; import { ArrowDownTrayIcon } from "@heroicons/react/24/solid";
import { FilterListContext, FilterListState } from "@utils/Context";
import { classNames } from "@utils";
import { FilterAddForm } from "@forms";
import { useToggle } from "@hooks/hooks";
import { APIClient } from "@api/APIClient";
import Toast from "@components/notifications/Toast";
import { EmptyListState } from "@components/emptystates";
import { DeleteModal } from "@components/modals";
export const filterKeys = {
all: ["filters"] as const,
lists: () => [...filterKeys.all, "list"] as const,
list: (indexers: string[], sortOrder: string) => [...filterKeys.lists(), { indexers, sortOrder }] as const,
details: () => [...filterKeys.all, "detail"] as const,
detail: (id: number) => [...filterKeys.details(), id] as const
};
enum ActionType { enum ActionType {
INDEXER_FILTER_CHANGE = "INDEXER_FILTER_CHANGE", INDEXER_FILTER_CHANGE = "INDEXER_FILTER_CHANGE",
INDEXER_FILTER_RESET = "INDEXER_FILTER_RESET", INDEXER_FILTER_RESET = "INDEXER_FILTER_RESET",
@ -63,11 +72,7 @@ const FilterListReducer = (state: FilterListState, action: Actions): FilterListS
} }
}; };
interface FilterProps { export default function Filters() {
values?: FormikValues;
}
export default function Filters({}: FilterProps){
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [createFilterIsOpen, setCreateFilterIsOpen] = useState(false); const [createFilterIsOpen, setCreateFilterIsOpen] = useState(false);
@ -115,7 +120,7 @@ export default function Filters({}: FilterProps){
await APIClient.filters.create(newFilter); await APIClient.filters.create(newFilter);
// Update the filter list // Update the filter list
queryClient.invalidateQueries("filters"); queryClient.invalidateQueries({ queryKey: filterKeys.lists() });
toast.custom((t) => <Toast type="success" body="Filter imported successfully." t={t} />); toast.custom((t) => <Toast type="success" body="Filter imported successfully." t={t} />);
setShowImportModal(false); setShowImportModal(false);
@ -250,11 +255,11 @@ function FilterList({ toggleCreateFilter }: any) {
filterListState filterListState
); );
const { error, data } = useQuery( const { data, error } = useQuery({
["filters", indexerFilter, sortOrder], queryKey: filterKeys.list(indexerFilter, sortOrder),
() => APIClient.filters.find(indexerFilter, sortOrder), queryFn: ({ queryKey }) => APIClient.filters.find(queryKey[2].indexers, queryKey[2].sortOrder),
{ refetchOnWindowFocus: false } refetchOnWindowFocus: false
); });
useEffect(() => { useEffect(() => {
FilterListContext.set({ indexerFilter, sortOrder, status }); FilterListContext.set({ indexerFilter, sortOrder, status });
@ -284,9 +289,13 @@ function FilterList({ toggleCreateFilter }: any) {
{data && data.length > 0 ? ( {data && data.length > 0 ? (
<ol className="min-w-full"> <ol className="min-w-full">
{filtered.filtered.map((filter: Filter, idx) => ( {filtered.filtered.length > 0
? filtered.filtered.map((filter: Filter, idx) => (
<FilterListItem filter={filter} values={filter} key={filter.id} idx={idx} /> <FilterListItem filter={filter} values={filter} key={filter.id} idx={idx} />
))} ))
: <EmptyListState text={`No ${status} filters`} />
}
</ol> </ol>
) : ( ) : (
<EmptyListState text="No filters here.." buttonText="Add new" buttonOnClick={toggleCreateFilter} /> <EmptyListState text="No filters here.." buttonText="Add new" buttonOnClick={toggleCreateFilter} />
@ -444,28 +453,25 @@ const FilterItemDropdown = ({ filter, onToggle }: FilterItemDropdownProps) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false); const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
const deleteMutation = useMutation(
(id: number) => APIClient.filters.delete(id), const deleteMutation = useMutation({
{ mutationFn: (id: number) => APIClient.filters.delete(id),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["filters"]); queryClient.invalidateQueries({ queryKey: filterKeys.lists() });
queryClient.invalidateQueries(["filters", filter.id]); queryClient.invalidateQueries({ queryKey: filterKeys.detail(filter.id) });
toast.custom((t) => <Toast type="success" body={`Filter ${filter?.name} was deleted`} t={t} />); toast.custom((t) => <Toast type="success" body={`Filter ${filter?.name} was deleted`} t={t} />);
} }
} });
);
const duplicateMutation = useMutation( const duplicateMutation = useMutation({
(id: number) => APIClient.filters.duplicate(id), mutationFn: (id: number) => APIClient.filters.duplicate(id),
{
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["filters"]); queryClient.invalidateQueries({ queryKey: filterKeys.lists() });
toast.custom((t) => <Toast type="success" body={`Filter ${filter?.name} duplicated`} t={t} />); toast.custom((t) => <Toast type="success" body={`Filter ${filter?.name} duplicated`} t={t} />);
} }
} });
);
return ( return (
<Menu as="div"> <Menu as="div">
@ -634,26 +640,22 @@ interface FilterListItemProps {
} }
function FilterListItem({ filter, values, idx }: FilterListItemProps) { function FilterListItem({ filter, values, idx }: FilterListItemProps) {
const [enabled, setEnabled] = useState(filter.enabled);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const updateMutation = useMutation( const updateMutation = useMutation({
(status: boolean) => APIClient.filters.toggleEnable(filter.id, status), mutationFn: (status: boolean) => APIClient.filters.toggleEnable(filter.id, status),
{
onSuccess: () => { onSuccess: () => {
toast.custom((t) => <Toast type="success" body={`${filter.name} was ${enabled ? "disabled" : "enabled"} successfully`} t={t} />); toast.custom((t) => <Toast type="success" body={`${filter.name} was ${!filter.enabled ? "disabled" : "enabled"} successfully`} t={t} />);
// We need to invalidate both keys here. // We need to invalidate both keys here.
// The filters key is used on the /filters page, // The filters key is used on the /filters page,
// while the ["filter", filter.id] key is used on the details page. // while the ["filter", filter.id] key is used on the details page.
queryClient.invalidateQueries(["filters"]); queryClient.invalidateQueries({ queryKey: filterKeys.lists() });
queryClient.invalidateQueries(["filters", filter?.id]); queryClient.invalidateQueries({ queryKey: filterKeys.detail(filter.id) });
} }
} });
);
const toggleActive = (status: boolean) => { const toggleActive = (status: boolean) => {
setEnabled(status);
updateMutation.mutate(status); updateMutation.mutate(status);
}; };
@ -671,10 +673,10 @@ function FilterListItem({ filter, values, idx }: FilterListItemProps) {
className="px-4 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-100" className="px-4 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-100"
> >
<Switch <Switch
checked={enabled} checked={filter.enabled}
onChange={toggleActive} onChange={toggleActive}
className={classNames( className={classNames(
enabled ? "bg-blue-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-700", filter.enabled ? "bg-blue-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-700",
"relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" "relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
)} )}
> >
@ -682,7 +684,7 @@ function FilterListItem({ filter, values, idx }: FilterListItemProps) {
<span <span
aria-hidden="true" aria-hidden="true"
className={classNames( className={classNames(
enabled ? "translate-x-5" : "translate-x-0", filter.enabled ? "translate-x-5" : "translate-x-0",
"inline-block h-5 w-5 rounded-full bg-white dark:bg-gray-200 shadow transform ring-0 transition ease-in-out duration-200" "inline-block h-5 w-5 rounded-full bg-white dark:bg-gray-200 shadow transform ring-0 transition ease-in-out duration-200"
)} )}
/> />
@ -730,7 +732,7 @@ function FilterListItem({ filter, values, idx }: FilterListItemProps) {
/> />
</span> </span>
<span className="text-sm text-gray-800 dark:text-gray-500"> <span className="text-sm text-gray-800 dark:text-gray-500">
<Tooltip style={{ width: "350px", fontSize: "12px", textTransform: "none", fontWeight: "normal", borderRadius: "0.375rem", backgroundColor: "#34343A", color: "#fff", opacity: "1", whiteSpace: "pre-wrap", overflow: "hidden", textOverflow: "ellipsis" }} delayShow={100} delayHide={150} data-html={true} place="right" anchorId={`tooltip-actions-${filter.id}`} html="<p>You need to setup an action in the filter otherwise you will not get any snatches.</p>" /> <Tooltip style={{ width: "350px", fontSize: "12px", textTransform: "none", fontWeight: "normal", borderRadius: "0.375rem", backgroundColor: "#34343A", color: "#fff", opacity: "1", whiteSpace: "pre-wrap", overflow: "hidden", textOverflow: "ellipsis" }} delayShow={100} delayHide={150} data-html={true} place="right" data-tooltip-id={`tooltip-actions-${filter.id}`} html="<p>You need to setup an action in the filter otherwise you will not get any snatches.</p>" />
</span> </span>
</> </>
)} )}
@ -848,14 +850,12 @@ const ListboxFilter = ({
// a unique option from a list // a unique option from a list
const IndexerSelectFilter = ({ dispatch }: any) => { const IndexerSelectFilter = ({ dispatch }: any) => {
const { data, isSuccess } = useQuery( const { data, isSuccess } = useQuery({
"indexers_options", queryKey: ["filters","indexers_options"],
() => APIClient.indexers.getOptions(), queryFn: () => APIClient.indexers.getOptions(),
{
keepPreviousData: true, keepPreviousData: true,
staleTime: Infinity staleTime: Infinity
} });
);
const setFilter = (value: string) => { const setFilter = (value: string) => {
if (value == undefined || value == "") { if (value == undefined || value == "") {

View file

@ -1,11 +1,11 @@
import * as React from "react"; import * as React from "react";
import { useQuery } from "react-query"; import { useQuery } from "@tanstack/react-query";
import { Listbox, Transition } from "@headlessui/react"; import { Listbox, Transition } from "@headlessui/react";
import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/solid"; import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/solid";
import { APIClient } from "../../api/APIClient"; import { APIClient } from "@api/APIClient";
import { classNames } from "../../utils"; import { classNames } from "@utils";
import { PushStatusOptions } from "../../domain/constants"; import { PushStatusOptions } from "@domain/constants";
import { FilterProps } from "react-table"; import { FilterProps } from "react-table";
import { DebounceInput } from "react-debounce-input"; import { DebounceInput } from "react-debounce-input";
@ -62,14 +62,12 @@ const ListboxFilter = ({
export const IndexerSelectColumnFilter = ({ export const IndexerSelectColumnFilter = ({
column: { filterValue, setFilter, id } column: { filterValue, setFilter, id }
}: FilterProps<object>) => { }: FilterProps<object>) => {
const { data, isSuccess } = useQuery( const { data, isSuccess } = useQuery({
"indexer_options", queryKey: ["indexer_options"],
() => APIClient.release.indexerOptions(), queryFn: () => APIClient.release.indexerOptions(),
{
keepPreviousData: true, keepPreviousData: true,
staleTime: Infinity staleTime: Infinity
} });
);
// Render a multi-select box // Render a multi-select box
return ( return (

View file

@ -1,5 +1,5 @@
import * as React from "react"; import * as React from "react";
import { useQuery } from "react-query"; import { useQuery } from "@tanstack/react-query";
import { CellProps, Column, useFilters, usePagination, useSortBy, useTable } from "react-table"; import { CellProps, Column, useFilters, usePagination, useSortBy, useTable } from "react-table";
import { import {
ChevronDoubleLeftIcon, ChevronDoubleLeftIcon,
@ -8,16 +8,25 @@ import {
ChevronRightIcon ChevronRightIcon
} from "@heroicons/react/24/solid"; } from "@heroicons/react/24/solid";
import { APIClient } from "../../api/APIClient"; import { APIClient } from "@api/APIClient";
import { EmptyListState } from "../../components/emptystates"; import { EmptyListState } from "@components/emptystates";
import * as Icons from "../../components/Icons"; import * as Icons from "@components/Icons";
import * as DataTable from "../../components/data-table"; import * as DataTable from "@components/data-table";
import { IndexerSelectColumnFilter, PushStatusSelectColumnFilter, SearchColumnFilter } from "./Filters"; import { IndexerSelectColumnFilter, PushStatusSelectColumnFilter, SearchColumnFilter } from "./Filters";
import { classNames } from "../../utils"; import { classNames } from "@utils";
import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline"; import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline";
import { Tooltip } from "../../components/tooltips/Tooltip"; import { Tooltip } from "@components/tooltips/Tooltip";
export const releaseKeys = {
all: ["releases"] as const,
lists: () => [...releaseKeys.all, "list"] as const,
list: (pageIndex: number, pageSize: number, filters: ReleaseFilter[]) => [...releaseKeys.lists(), { pageIndex, pageSize, filters }] as const,
details: () => [...releaseKeys.all, "detail"] as const,
detail: (id: number) => [...releaseKeys.details(), id] as const
};
type TableState = { type TableState = {
queryPageIndex: number; queryPageIndex: number;
@ -120,14 +129,12 @@ export const ReleaseTable = () => {
const [{ queryPageIndex, queryPageSize, totalCount, queryFilters }, dispatch] = const [{ queryPageIndex, queryPageSize, totalCount, queryFilters }, dispatch] =
React.useReducer(TableReducer, initialState); React.useReducer(TableReducer, initialState);
const { isLoading, error, data, isSuccess } = useQuery( const { isLoading, error, data, isSuccess } = useQuery({
["releases", queryPageIndex, queryPageSize, queryFilters], queryKey: releaseKeys.list(queryPageIndex, queryPageSize, queryFilters),
() => APIClient.release.findQuery(queryPageIndex * queryPageSize, queryPageSize, queryFilters), queryFn: () => APIClient.release.findQuery(queryPageIndex * queryPageSize, queryPageSize, queryFilters),
{
keepPreviousData: true, keepPreviousData: true,
staleTime: 5000 staleTime: 5000
} });
);
// Use the state and functions returned from useTable to build your UI // Use the state and functions returned from useTable to build your UI
const { const {
@ -192,10 +199,11 @@ export const ReleaseTable = () => {
dispatch({ type: ActionType.FILTER_CHANGED, payload: filters }); dispatch({ type: ActionType.FILTER_CHANGED, payload: filters });
}, [filters]); }, [filters]);
if (error) if (error) {
return <p>Error</p>; return <p>Error</p>;
}
if (isLoading) if (isLoading) {
return ( return (
<div className="flex flex-col animate-pulse"> <div className="flex flex-col animate-pulse">
<div className="flex mb-6 flex-col sm:flex-row"> <div className="flex mb-6 flex-col sm:flex-row">
@ -211,9 +219,7 @@ export const ReleaseTable = () => {
<table {...getTableProps()} className="min-w-full divide-y divide-gray-200 dark:divide-gray-700"> <table {...getTableProps()} className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-800"> <thead className="bg-gray-50 dark:bg-gray-800">
<tr> <tr>
<th <th
scope="col" scope="col"
className="first:pl-5 pl-3 pr-3 py-3 first:rounded-tl-md last:rounded-tr-md text-xs font-medium tracking-wider text-left text-gray-500 uppercase group" className="first:pl-5 pl-3 pr-3 py-3 first:rounded-tl-md last:rounded-tr-md text-xs font-medium tracking-wider text-left text-gray-500 uppercase group"
@ -224,28 +230,30 @@ export const ReleaseTable = () => {
</span> </span>
</div> </div>
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody className=" divide-gray-200 dark:divide-gray-700"> <tbody className=" divide-gray-200 dark:divide-gray-700">
<tr className="flex justify-between py-4 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]"> <tr
className="flex justify-between py-4 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td> <td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td> <td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td> <td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
</tr> </tr>
<tr className="flex justify-between py-4 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]"> <tr
className="flex justify-between py-4 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td> <td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td> <td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td> <td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
</tr> </tr>
<tr className="flex justify-between py-4 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]"> <tr
className="flex justify-between py-4 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td> <td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td> <td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap">&nbsp;</td> <td className="first:pl-5 pl-3 pr-3 whitespace-nowrap">&nbsp;</td>
</tr> </tr>
<tr className="flex justify-between py-4 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]"> <tr
className="flex justify-between py-4 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td> <td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td> <td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td> <td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
@ -255,27 +263,32 @@ export const ReleaseTable = () => {
<p className="text-black dark:text-white">Loading release table...</p> <p className="text-black dark:text-white">Loading release table...</p>
</td> </td>
</tr> </tr>
<tr className="flex justify-between py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]"> <tr
className="flex justify-between py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap">&nbsp;</td> <td className="first:pl-5 pl-3 pr-3 whitespace-nowrap">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td> <td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td> <td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
</tr> </tr>
<tr className="flex justify-between py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]"> <tr
className="flex justify-between py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td> <td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td> <td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td> <td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
</tr> </tr>
<tr className="flex justify-between py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]"> <tr
className="flex justify-between py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td> <td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td> <td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td> <td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
</tr> </tr>
<tr className="flex justify-between py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]"> <tr
className="flex justify-between py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td> <td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td> <td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td> <td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
</tr> </tr>
<tr className="flex justify-between py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]"> <tr
className="flex justify-between py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]">
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td> <td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td> <td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
<td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td> <td className="first:pl-5 pl-3 pr-3 whitespace-nowrap ">&nbsp;</td>
@ -292,7 +305,8 @@ export const ReleaseTable = () => {
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between"> <div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div className="flex items-baseline gap-x-2"> <div className="flex items-baseline gap-x-2">
<span className="text-sm text-gray-700 dark:text-gray-500"> <span className="text-sm text-gray-700 dark:text-gray-500">
Page <span className="font-medium">{pageIndex + 1}</span> of <span className="font-medium">{pageOptions.length}</span> Page <span className="font-medium">{pageIndex + 1}</span> of <span
className="font-medium">{pageOptions.length}</span>
</span> </span>
<label> <label>
<span className="sr-only bg-gray-700">Items Per Page</span> <span className="sr-only bg-gray-700">Items Per Page</span>
@ -349,9 +363,11 @@ export const ReleaseTable = () => {
</div> </div>
</div> </div>
); );
}
if (!data) if (!data) {
return <EmptyListState text="No recent activity" />; return <EmptyListState text="No recent activity" />;
}
// Render the UI for your table // Render the UI for your table
return ( return (

View file

@ -1,20 +1,31 @@
import { useRef } from "react"; import { useRef } from "react";
import { useMutation, useQuery, useQueryClient } from "react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { KeyField } from "../../components/fields/text";
import { DeleteModal } from "../../components/modals";
import APIKeyAddForm from "../../forms/settings/APIKeyAddForm";
import Toast from "../../components/notifications/Toast";
import { APIClient } from "../../api/APIClient";
import { useToggle } from "../../hooks/hooks";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { classNames } from "../../utils";
import { TrashIcon } from "@heroicons/react/24/outline"; import { TrashIcon } from "@heroicons/react/24/outline";
import { EmptySimple } from "../../components/emptystates";
import { KeyField } from "@components/fields/text";
import { DeleteModal } from "@components/modals";
import APIKeyAddForm from "@forms/settings/APIKeyAddForm";
import Toast from "@components/notifications/Toast";
import { APIClient } from "@api/APIClient";
import { useToggle } from "@hooks/hooks";
import { classNames } from "@utils";
import { EmptySimple } from "@components/emptystates";
export const apiKeys = {
all: ["api_keys"] as const,
lists: () => [...apiKeys.all, "list"] as const,
details: () => [...apiKeys.all, "detail"] as const,
// detail: (id: number) => [...apiKeys.details(), id] as const
detail: (id: string) => [...apiKeys.details(), id] as const
};
function APISettings() { function APISettings() {
const [addFormIsOpen, toggleAddForm] = useToggle(false); const [addFormIsOpen, toggleAddForm] = useToggle(false);
const { data } = useQuery(["apikeys"], () => APIClient.apikeys.getAll(), { const { data } = useQuery({
queryKey: apiKeys.lists(),
queryFn: APIClient.apikeys.getAll,
retry: false, retry: false,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
onError: (err) => console.log(err) onError: (err) => console.log(err)
@ -57,7 +68,7 @@ function APISettings() {
</div> </div>
</li> </li>
{data && data.map((k) => <APIListItem key={k.key} apikey={k} />)} {data && data.map((k, idx) => <APIListItem key={idx} apikey={k} />)}
</ol> </ol>
</section> </section>
) : ( ) : (
@ -83,12 +94,11 @@ function APIListItem({ apikey }: ApiKeyItemProps) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const deleteMutation = useMutation( const deleteMutation = useMutation({
(key: string) => APIClient.apikeys.delete(key), mutationFn: (key: string) => APIClient.apikeys.delete(key),
{
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["apikeys"]); queryClient.invalidateQueries({ queryKey: apiKeys.lists() });
queryClient.invalidateQueries(["apikeys", apikey.key]); queryClient.invalidateQueries({ queryKey: apiKeys.detail(apikey.key) });
toast.custom((t) => ( toast.custom((t) => (
<Toast <Toast
@ -98,8 +108,7 @@ function APIListItem({ apikey }: ApiKeyItemProps) {
/> />
)); ));
} }
} });
);
return ( return (
<li className="text-gray-500 dark:text-gray-400"> <li className="text-gray-500 dark:text-gray-400">

View file

@ -1,10 +1,11 @@
import { useMutation, useQuery, useQueryClient } from "react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { APIClient } from "../../api/APIClient";
import { Checkbox } from "../../components/Checkbox";
import { SettingsContext } from "../../utils/Context";
import { GithubRelease } from "../../types/Update";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import Toast from "../../components/notifications/Toast";
import { APIClient } from "@api/APIClient";
import { Checkbox } from "@components/Checkbox";
import { SettingsContext } from "@utils/Context";
import { GithubRelease } from "@app/types/Update";
import Toast from "@components/notifications/Toast";
interface RowItemProps { interface RowItemProps {
label: string; label: string;
@ -47,8 +48,9 @@ const RowItemNumber = ({ label, value, title, unit }: RowItemNumberProps) => {
}; };
const RowItemVersion = ({ label, value, title, newUpdate }: RowItemProps) => { const RowItemVersion = ({ label, value, title, newUpdate }: RowItemProps) => {
if (!value) if (!value) {
return null; return null;
}
return ( return (
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6"> <div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
@ -68,49 +70,41 @@ const RowItemVersion = ({ label, value, title, newUpdate }: RowItemProps) => {
function ApplicationSettings() { function ApplicationSettings() {
const [settings, setSettings] = SettingsContext.use(); const [settings, setSettings] = SettingsContext.use();
const { isLoading, data } = useQuery( const { isLoading, data } = useQuery({
["config"], queryKey: ["config"],
() => APIClient.config.get(), queryFn: APIClient.config.get,
{
retry: false, retry: false,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
onError: err => console.log(err) onError: err => console.log(err)
} });
);
const { data: updateData } = useQuery( const { data: updateData } = useQuery({
["updates"], queryKey: ["updates"],
() => APIClient.updates.getLatestRelease(), queryFn: APIClient.updates.getLatestRelease,
{
retry: false, retry: false,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
onError: err => console.log(err) onError: err => console.log(err)
} });
);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const checkUpdateMutation = useMutation( const checkUpdateMutation = useMutation({
() => APIClient.updates.check(), mutationFn: APIClient.updates.check,
{
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["updates"]); queryClient.invalidateQueries({ queryKey: ["updates"] });
} }
} });
);
const toggleCheckUpdateMutation = useMutation( const toggleCheckUpdateMutation = useMutation({
(value: boolean) => APIClient.config.update({ check_for_updates: value }), mutationFn: (value: boolean) => APIClient.config.update({ check_for_updates: value }),
{
onSuccess: () => { onSuccess: () => {
toast.custom((t) => <Toast type="success" body={"Config successfully updated!"} t={t}/>); toast.custom((t) => <Toast type="success" body={"Config successfully updated!"} t={t}/>);
queryClient.invalidateQueries(["config"]); queryClient.invalidateQueries({ queryKey: ["config"] });
checkUpdateMutation.mutate(); checkUpdateMutation.mutate();
} }
} });
);
return ( return (
<div className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9"> <div className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9">

View file

@ -1,14 +1,23 @@
import { useToggle } from "../../hooks/hooks";
import { Switch } from "@headlessui/react";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { classNames } from "../../utils";
import { DownloadClientAddForm, DownloadClientUpdateForm } from "../../forms";
import { EmptySimple } from "../../components/emptystates";
import { APIClient } from "../../api/APIClient";
import { DownloadClientTypeNameMap } from "../../domain/constants";
import toast from "react-hot-toast";
import Toast from "../../components/notifications/Toast";
import { useState, useMemo } from "react"; import { useState, useMemo } from "react";
import { Switch } from "@headlessui/react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import toast from "react-hot-toast";
import { useToggle } from "@hooks/hooks";
import { classNames } from "@utils";
import { DownloadClientAddForm, DownloadClientUpdateForm } from "@forms";
import { EmptySimple } from "@components/emptystates";
import { APIClient } from "@api/APIClient";
import { DownloadClientTypeNameMap } from "@domain/constants";
import Toast from "@components/notifications/Toast";
export const clientKeys = {
all: ["download_clients"] as const,
lists: () => [...clientKeys.all, "list"] as const,
// list: (indexers: string[], sortOrder: string) => [...clientKeys.lists(), { indexers, sortOrder }] as const,
details: () => [...clientKeys.all, "detail"] as const,
detail: (id: number) => [...clientKeys.details(), id] as const
};
interface DLSettingsItemProps { interface DLSettingsItemProps {
client: DownloadClient; client: DownloadClient;
@ -62,7 +71,6 @@ function useSort(items: ListItemProps["clients"][], config?: SortConfig) {
setSortConfig({ key, direction }); setSortConfig({ key, direction });
}; };
const getSortIndicator = (key: keyof ListItemProps["clients"]) => { const getSortIndicator = (key: keyof ListItemProps["clients"]) => {
if (!sortConfig || sortConfig.key !== key) { if (!sortConfig || sortConfig.key !== key) {
return ""; return "";
@ -79,15 +87,14 @@ function DownloadClientSettingsListItem({ client }: DLSettingsItemProps) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const mutation = useMutation( const mutation = useMutation({
(client: DownloadClient) => APIClient.download_clients.update(client), mutationFn: (client: DownloadClient) => APIClient.download_clients.update(client),
{
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["downloadClients"]);
toast.custom((t) => <Toast type="success" body={`${client.name} was updated successfully`} t={t}/>); toast.custom((t) => <Toast type="success" body={`${client.name} was updated successfully`} t={t}/>);
queryClient.invalidateQueries({ queryKey: clientKeys.lists() });
} }
} });
);
const onToggleMutation = (newState: boolean) => { const onToggleMutation = (newState: boolean) => {
mutation.mutate({ mutation.mutate({
@ -139,11 +146,11 @@ function DownloadClientSettingsListItem({ client }: DLSettingsItemProps) {
function DownloadClientSettings() { function DownloadClientSettings() {
const [addClientIsOpen, toggleAddClient] = useToggle(false); const [addClientIsOpen, toggleAddClient] = useToggle(false);
const { error, data } = useQuery( const { error, data } = useQuery({
"downloadClients", queryKey: clientKeys.lists(),
() => APIClient.download_clients.getAll(), queryFn: APIClient.download_clients.getAll,
{ refetchOnWindowFocus: false } refetchOnWindowFocus: false
); });
const sortedClients = useSort(data || []); const sortedClients = useSort(data || []);
@ -151,10 +158,8 @@ function DownloadClientSettings() {
return <p>Failed to fetch download clients</p>; return <p>Failed to fetch download clients</p>;
} }
return ( return (
<div className="lg:col-span-9"> <div className="lg:col-span-9">
<DownloadClientAddForm isOpen={addClientIsOpen} toggle={toggleAddClient} /> <DownloadClientAddForm isOpen={addClientIsOpen} toggle={toggleAddClient} />
<div className="py-6 px-2 lg:pb-8"> <div className="py-6 px-2 lg:pb-8">
@ -177,8 +182,8 @@ function DownloadClientSettings() {
</div> </div>
<div className="flex flex-col mt-6 px-4"> <div className="flex flex-col mt-6 px-4">
{sortedClients.items.length > 0 ? {sortedClients.items.length > 0
<section className="light:bg-white dark:bg-gray-800 light:shadow sm:rounded-sm"> ? <section className="light:bg-white dark:bg-gray-800 light:shadow sm:rounded-sm">
<ol className="min-w-full relative"> <ol className="min-w-full relative">
<li className="grid grid-cols-12 border-b border-gray-200 dark:border-gray-700"> <li className="grid grid-cols-12 border-b border-gray-200 dark:border-gray-700">
<div className="flex col-span-2 sm:col-span-1 px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer" <div className="flex col-span-2 sm:col-span-1 px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
@ -213,7 +218,6 @@ function DownloadClientSettings() {
</div> </div>
</div> </div>
</div> </div>
); );
} }

View file

@ -1,13 +1,7 @@
import { useToggle } from "../../hooks/hooks";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { APIClient } from "../../api/APIClient";
import { Menu, Switch, Transition } from "@headlessui/react";
import { baseUrl, classNames, IsEmptyDate, simplifyDate } from "../../utils";
import { Fragment, useRef, useState, useMemo } from "react"; import { Fragment, useRef, useState, useMemo } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Menu, Switch, Transition } from "@headlessui/react";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import Toast from "../../components/notifications/Toast";
import { DeleteModal } from "../../components/modals";
import { import {
ArrowsRightLeftIcon, ArrowsRightLeftIcon,
DocumentTextIcon, DocumentTextIcon,
@ -15,10 +9,24 @@ import {
PencilSquareIcon, PencilSquareIcon,
TrashIcon TrashIcon
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { FeedUpdateForm } from "../../forms/settings/FeedForms";
import { EmptySimple } from "../../components/emptystates"; import { APIClient } from "@api/APIClient";
import { useToggle } from "@hooks/hooks";
import { baseUrl, classNames, IsEmptyDate, simplifyDate } from "@utils";
import Toast from "@components/notifications/Toast";
import { DeleteModal } from "@components/modals";
import { FeedUpdateForm } from "@forms/settings/FeedForms";
import { EmptySimple } from "@components/emptystates";
import { ImplementationBadges } from "./Indexer"; import { ImplementationBadges } from "./Indexer";
export const feedKeys = {
all: ["feeds"] as const,
lists: () => [...feedKeys.all, "list"] as const,
// list: (indexers: string[], sortOrder: string) => [...feedKeys.lists(), { indexers, sortOrder }] as const,
details: () => [...feedKeys.all, "detail"] as const,
detail: (id: number) => [...feedKeys.details(), id] as const
};
interface SortConfig { interface SortConfig {
key: keyof ListItemProps["feed"] | "enabled"; key: keyof ListItemProps["feed"] | "enabled";
direction: "ascending" | "descending"; direction: "ascending" | "descending";
@ -27,8 +35,6 @@ interface SortConfig {
function useSort(items: ListItemProps["feed"][], config?: SortConfig) { function useSort(items: ListItemProps["feed"][], config?: SortConfig) {
const [sortConfig, setSortConfig] = useState(config); const [sortConfig, setSortConfig] = useState(config);
const sortedItems = useMemo(() => { const sortedItems = useMemo(() => {
if (!sortConfig) { if (!sortConfig) {
return items; return items;
@ -76,11 +82,11 @@ function useSort(items: ListItemProps["feed"][], config?: SortConfig) {
} }
function FeedSettings() { function FeedSettings() {
const { data } = useQuery( const { data } = useQuery({
"feeds", queryKey: feedKeys.lists(),
() => APIClient.feeds.find(), queryFn: APIClient.feeds.find,
{ refetchOnWindowFocus: false } refetchOnWindowFocus: false
); });
const sortedFeeds = useSort(data || []); const sortedFeeds = useSort(data || []);
@ -142,19 +148,15 @@ function ListItem({ feed }: ListItemProps) {
const [enabled, setEnabled] = useState(feed.enabled); const [enabled, setEnabled] = useState(feed.enabled);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const updateMutation = useMutation( const updateMutation = useMutation({
(status: boolean) => APIClient.feeds.toggleEnable(feed.id, status), mutationFn: (status: boolean) => APIClient.feeds.toggleEnable(feed.id, status),
{
onSuccess: () => { onSuccess: () => {
toast.custom((t) => <Toast type="success" queryClient.invalidateQueries({ queryKey: feedKeys.lists() });
body={`${feed.name} was ${enabled ? "disabled" : "enabled"} successfully`} queryClient.invalidateQueries({ queryKey: feedKeys.detail(feed.id) });
t={t}/>);
queryClient.invalidateQueries(["feeds"]); toast.custom((t) => <Toast type="success" body={`${feed.name} was ${!enabled ? "disabled" : "enabled"} successfully`} t={t}/>);
queryClient.invalidateQueries(["feeds", feed?.id]);
} }
} });
);
const toggleActive = (status: boolean) => { const toggleActive = (status: boolean) => {
setEnabled(status); setEnabled(status);
@ -227,17 +229,15 @@ const FeedItemDropdown = ({
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false); const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
const deleteMutation = useMutation( const deleteMutation = useMutation({
(id: number) => APIClient.feeds.delete(id), mutationFn: (id: number) => APIClient.feeds.delete(id),
{
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["feeds"]); queryClient.invalidateQueries({ queryKey: feedKeys.lists() });
queryClient.invalidateQueries(["feeds", feed.id]); queryClient.invalidateQueries({ queryKey: feedKeys.detail(feed.id) });
toast.custom((t) => <Toast type="success" body={`Feed ${feed?.name} was deleted`} t={t}/>); toast.custom((t) => <Toast type="success" body={`Feed ${feed?.name} was deleted`} t={t}/>);
} }
} });
);
return ( return (
<Menu as="div"> <Menu as="div">

View file

@ -1,12 +1,21 @@
import { useToggle } from "../../hooks/hooks";
import { useQuery } from "react-query";
import { IndexerAddForm, IndexerUpdateForm } from "../../forms";
import { Switch } from "@headlessui/react";
import { classNames } from "../../utils";
import { EmptySimple } from "../../components/emptystates";
import { APIClient } from "../../api/APIClient";
import { componentMapType } from "../../forms/settings/DownloadClientForms";
import { useState, useMemo } from "react"; import { useState, useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
import { Switch } from "@headlessui/react";
import { IndexerAddForm, IndexerUpdateForm } from "@forms";
import { useToggle } from "@hooks/hooks";
import { classNames } from "@utils";
import { EmptySimple } from "@components/emptystates";
import { APIClient } from "@api/APIClient";
import { componentMapType } from "@forms/settings/DownloadClientForms";
export const indexerKeys = {
all: ["indexers"] as const,
lists: () => [...indexerKeys.all, "list"] as const,
// list: (indexers: string[], sortOrder: string) => [...indexerKeys.lists(), { indexers, sortOrder }] as const,
details: () => [...indexerKeys.all, "detail"] as const,
detail: (id: number) => [...indexerKeys.details(), id] as const
};
interface SortConfig { interface SortConfig {
key: keyof ListItemProps["indexer"] | "enabled"; key: keyof ListItemProps["indexer"] | "enabled";
@ -149,16 +158,17 @@ const ListItem = ({ indexer }: ListItemProps) => {
function IndexerSettings() { function IndexerSettings() {
const [addIndexerIsOpen, toggleAddIndexer] = useToggle(false); const [addIndexerIsOpen, toggleAddIndexer] = useToggle(false);
const { error, data } = useQuery( const { error, data } = useQuery({
"indexer", queryKey: indexerKeys.lists(),
() => APIClient.indexers.getAll(), queryFn: APIClient.indexers.getAll,
{ refetchOnWindowFocus: false } refetchOnWindowFocus: false
); });
const sortedIndexers = useSort(data || []); const sortedIndexers = useSort(data || []);
if (error) if (error) {
return (<p>An error has occurred</p>); return (<p>An error has occurred</p>);
}
return ( return (
<div className="lg:col-span-9"> <div className="lg:col-span-9">

View file

@ -1,16 +1,8 @@
import { useMutation, useQuery, useQueryClient } from "react-query"; import { Fragment, useRef, useState, useMemo } from "react";
import { classNames, IsEmptyDate, simplifyDate } from "../../utils"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { IrcNetworkAddForm, IrcNetworkUpdateForm } from "../../forms";
import { useToggle } from "../../hooks/hooks";
import { APIClient } from "../../api/APIClient";
import { EmptySimple } from "../../components/emptystates";
import { LockClosedIcon, LockOpenIcon } from "@heroicons/react/24/solid"; import { LockClosedIcon, LockOpenIcon } from "@heroicons/react/24/solid";
import { Menu, Switch, Transition } from "@headlessui/react"; import { Menu, Switch, Transition } from "@headlessui/react";
import { Fragment, useRef } from "react";
import { DeleteModal } from "../../components/modals";
import { useState, useMemo } from "react";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import Toast from "../../components/notifications/Toast";
import { import {
ArrowsPointingInIcon, ArrowsPointingInIcon,
ArrowsPointingOutIcon, ArrowsPointingOutIcon,
@ -20,6 +12,22 @@ import {
TrashIcon TrashIcon
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { classNames, IsEmptyDate, simplifyDate } from "@utils";
import { IrcNetworkAddForm, IrcNetworkUpdateForm } from "@forms";
import { useToggle } from "@hooks/hooks";
import { APIClient } from "@api/APIClient";
import { EmptySimple } from "@components/emptystates";
import { DeleteModal } from "@components/modals";
import Toast from "@components/notifications/Toast";
export const ircKeys = {
all: ["irc_networks"] as const,
lists: () => [...ircKeys.all, "list"] as const,
// list: (indexers: string[], sortOrder: string) => [...ircKeys.lists(), { indexers, sortOrder }] as const,
details: () => [...ircKeys.all, "detail"] as const,
detail: (id: number) => [...ircKeys.details(), id] as const
};
interface SortConfig { interface SortConfig {
key: keyof ListItemProps["network"] | "enabled"; key: keyof ListItemProps["network"] | "enabled";
direction: "ascending" | "descending"; direction: "ascending" | "descending";
@ -79,10 +87,11 @@ const IrcSettings = () => {
const [expandNetworks, toggleExpand] = useToggle(false); const [expandNetworks, toggleExpand] = useToggle(false);
const [addNetworkIsOpen, toggleAddNetwork] = useToggle(false); const [addNetworkIsOpen, toggleAddNetwork] = useToggle(false);
const { data } = useQuery("networks", () => APIClient.irc.getNetworks(), { const { data } = useQuery({
queryKey: ircKeys.lists(),
queryFn: APIClient.irc.getNetworks,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
// Refetch every 3 seconds refetchInterval: 3000 // Refetch every 3 seconds
refetchInterval: 3000
}); });
const sortedNetworks = useSort(data || []); const sortedNetworks = useSort(data || []);
@ -204,18 +213,17 @@ const ListItem = ({ idx, network, expanded }: ListItemProps) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const mutation = useMutation( const updateMutation = useMutation({
(network: IrcNetwork) => APIClient.irc.updateNetwork(network), mutationFn: (network: IrcNetwork) => APIClient.irc.updateNetwork(network),
{
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["networks"]); queryClient.invalidateQueries({ queryKey: ircKeys.lists() });
toast.custom((t) => <Toast type="success" body={`${network.name} was updated successfully`} t={t}/>); toast.custom((t) => <Toast type="success" body={`${network.name} was updated successfully`} t={t}/>);
} }
} });
);
const onToggleMutation = (newState: boolean) => { const onToggleMutation = (newState: boolean) => {
mutation.mutate({ updateMutation.mutate({
...network, ...network,
enabled: newState enabled: newState
}); });
@ -399,37 +407,30 @@ const ListItemDropdown = ({
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false); const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
const deleteMutation = useMutation(
(id: number) => APIClient.irc.deleteNetwork(id), const deleteMutation = useMutation({
{ mutationFn: (id: number) => APIClient.irc.deleteNetwork(id),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["networks"]); queryClient.invalidateQueries({ queryKey: ircKeys.lists() });
queryClient.invalidateQueries(["networks", network.id]); queryClient.invalidateQueries({ queryKey: ircKeys.detail(network.id) });
toast.custom((t) => <Toast type="success" body={`Network ${network.name} was deleted`} t={t}/>); toast.custom((t) => <Toast type="success" body={`Network ${network.name} was deleted`} t={t}/>);
toggleDeleteModal(); toggleDeleteModal();
} }
} });
);
const restartMutation = useMutation( const restartMutation = useMutation({
(id: number) => APIClient.irc.restartNetwork(id), mutationFn: (id: number) => APIClient.irc.restartNetwork(id),
{
onSuccess: () => { onSuccess: () => {
toast.custom((t) => <Toast type="success" queryClient.invalidateQueries({ queryKey: ircKeys.lists() });
body={`${network.name} was successfully restarted`} queryClient.invalidateQueries({ queryKey: ircKeys.detail(network.id) });
t={t}/>);
queryClient.invalidateQueries(["networks"]); toast.custom((t) => <Toast type="success" body={`${network.name} was successfully restarted`} t={t}/>);
queryClient.invalidateQueries(["networks", network.id]);
} }
} });
);
const restart = (id: number) => { const restart = (id: number) => restartMutation.mutate(id);
restartMutation.mutate(id);
};
return ( return (
<Menu as="div"> <Menu as="div">

View file

@ -1,10 +1,11 @@
import { useMutation, useQuery, useQueryClient } from "react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { APIClient } from "../../api/APIClient";
import { GithubRelease } from "../../types/Update";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import Toast from "../../components/notifications/Toast";
import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select"; import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select";
import { LogLevelOptions, SelectOption } from "../../domain/constants";
import { APIClient } from "@api/APIClient";
import { GithubRelease } from "@app/types/Update";
import Toast from "@components/notifications/Toast";
import { LogLevelOptions, SelectOption } from "@domain/constants";
import { LogFiles } from "../Logs"; import { LogFiles } from "../Logs";
interface RowItemProps { interface RowItemProps {
@ -121,28 +122,24 @@ const RowItemSelect = ({ id, title, label, value, options, onChange }: any) => {
}; };
function LogSettings() { function LogSettings() {
const { isLoading, data } = useQuery( const { isLoading, data } = useQuery({
["config"], queryKey: ["config"],
() => APIClient.config.get(), queryFn: APIClient.config.get,
{
retry: false, retry: false,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
onError: err => console.log(err) onError: err => console.log(err)
} });
);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const setLogLevelUpdateMutation = useMutation( const setLogLevelUpdateMutation = useMutation({
(value: string) => APIClient.config.update({ log_level: value }), mutationFn: (value: string) => APIClient.config.update({ log_level: value }),
{
onSuccess: () => { onSuccess: () => {
toast.custom((t) => <Toast type="success" body={"Config successfully updated!"} t={t}/>); toast.custom((t) => <Toast type="success" body={"Config successfully updated!"} t={t}/>);
queryClient.invalidateQueries(["config"]); queryClient.invalidateQueries({ queryKey: ["config"] });
} }
} });
);
return ( return (
<div className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9"> <div className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9">

View file

@ -1,19 +1,27 @@
import { useQuery } from "react-query"; import { useQuery } from "@tanstack/react-query";
import { APIClient } from "../../api/APIClient";
import { EmptySimple } from "../../components/emptystates";
import { useToggle } from "../../hooks/hooks";
import { NotificationAddForm, NotificationUpdateForm } from "../../forms/settings/NotificationForms";
import { Switch } from "@headlessui/react"; import { Switch } from "@headlessui/react";
import { classNames } from "../../utils";
import { componentMapType } from "../../forms/settings/DownloadClientForms"; import { APIClient } from "@api/APIClient";
import { EmptySimple } from "@components/emptystates";
import { useToggle } from "@hooks/hooks";
import { NotificationAddForm, NotificationUpdateForm } from "@forms/settings/NotificationForms";
import { classNames } from "@utils";
import { componentMapType } from "@forms/settings/DownloadClientForms";
export const notificationKeys = {
all: ["notifications"] as const,
lists: () => [...notificationKeys.all, "list"] as const,
details: () => [...notificationKeys.all, "detail"] as const,
detail: (id: number) => [...notificationKeys.details(), id] as const
};
function NotificationSettings() { function NotificationSettings() {
const [addNotificationsIsOpen, toggleAddNotifications] = useToggle(false); const [addNotificationsIsOpen, toggleAddNotifications] = useToggle(false);
const { data } = useQuery( const { data } = useQuery({
"notifications", queryKey: notificationKeys.lists(),
() => APIClient.notifications.getAll(), queryFn: APIClient.notifications.getAll,
{ refetchOnWindowFocus: false } refetchOnWindowFocus: false }
); );
return ( return (

View file

@ -1,29 +1,30 @@
import { useRef } from "react"; import { useRef } from "react";
import { useMutation, useQueryClient } from "react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { APIClient } from "../../api/APIClient";
import Toast from "../../components/notifications/Toast"; import { APIClient } from "@api/APIClient";
import { useToggle } from "../../hooks/hooks"; import Toast from "@components/notifications/Toast";
import { DeleteModal } from "../../components/modals"; import { useToggle } from "@hooks/hooks";
import { DeleteModal } from "@components/modals";
import { releaseKeys } from "@screens/releases/ReleaseTable";
function ReleaseSettings() { function ReleaseSettings() {
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false); const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const deleteMutation = useMutation(() => APIClient.release.delete(), { const deleteMutation = useMutation({
mutationFn: APIClient.release.delete,
onSuccess: () => { onSuccess: () => {
toast.custom((t) => ( toast.custom((t) => (
<Toast type="success" body={"All releases were deleted"} t={t}/> <Toast type="success" body={"All releases were deleted"} t={t}/>
)); ));
// Invalidate filters just in case, most likely not necessary but can't hurt. // Invalidate filters just in case, most likely not necessary but can't hurt.
queryClient.invalidateQueries("releases"); queryClient.invalidateQueries({ queryKey: releaseKeys.lists() });
} }
}); });
const deleteAction = () => { const deleteAction = () => deleteMutation.mutate();
deleteMutation.mutate();
};
const cancelModalButtonRef = useRef(null); const cancelModalButtonRef = useRef(null);

View file

@ -1,6 +1,5 @@
import { newRidgeState } from "react-ridge-state"; import { newRidgeState } from "react-ridge-state";
export const InitializeGlobalContext = () => { export const InitializeGlobalContext = () => {
const auth_ctx = localStorage.getItem("auth"); const auth_ctx = localStorage.getItem("auth");
if (auth_ctx) if (auth_ctx)

View file

@ -21,7 +21,8 @@
"jsx": "react-jsx", "jsx": "react-jsx",
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"paths": { "paths": {
"@/*": ["./src/*"] "@*": ["./src/*"],
"@app*": ["./src/*"]
} }
}, },
"include": [ "include": [

View file

@ -12,9 +12,18 @@ export default ({ mode }: { mode: any }) => {
base: "", base: "",
plugins: [react()], plugins: [react()],
resolve: { resolve: {
alias: { alias: [
"@": fileURLToPath(new URL("./src", import.meta.url)) { find: "@", replacement: fileURLToPath(new URL("./src/", import.meta.url)) },
} { find: "@app", replacement: fileURLToPath(new URL("./src/", import.meta.url)) },
{ find: "@components", replacement: fileURLToPath(new URL("./src/components", import.meta.url)) },
{ find: "@forms", replacement: fileURLToPath(new URL("./src/forms", import.meta.url)) },
{ find: "@hooks", replacement: fileURLToPath(new URL("./src/hooks", import.meta.url)) },
{ find: "@api", replacement: fileURLToPath(new URL("./src/api", import.meta.url)) },
{ find: "@screens", replacement: fileURLToPath(new URL("./src/screens", import.meta.url)) },
{ find: "@utils", replacement: fileURLToPath(new URL("./src/utils", import.meta.url)) },
{ find: "@types", replacement: fileURLToPath(new URL("./src/types", import.meta.url)) },
{ find: "@domain", replacement: fileURLToPath(new URL("./src/domain", import.meta.url)) }
]
}, },
server: { server: {
port: 3000, port: 3000,

View file

@ -48,7 +48,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.2, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.8.7": "@babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7":
version: 7.21.0 version: 7.21.0
resolution: "@babel/runtime@npm:7.21.0" resolution: "@babel/runtime@npm:7.21.0"
dependencies: dependencies:
@ -703,6 +703,56 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@tanstack/match-sorter-utils@npm:^8.7.0":
version: 8.8.4
resolution: "@tanstack/match-sorter-utils@npm:8.8.4"
dependencies:
remove-accents: 0.4.2
checksum: d005f500754f52ef94966cbbe4217f26e7e3c07291faa2578b06bca9a5abe01689569994c37a1d01c6e783addf5ffbb28fa82eba7961d36eabf43ec43d1e496b
languageName: node
linkType: hard
"@tanstack/query-core@npm:4.29.1":
version: 4.29.1
resolution: "@tanstack/query-core@npm:4.29.1"
checksum: e7a5a73e5e743411e39348bee3ded4b6c5dd3a70009e72e067e257d5f3d612bfe8df0998cddff6ad7a078e0bcf7a3c92ded669758ca697055472ce910e23a368
languageName: node
linkType: hard
"@tanstack/react-query-devtools@npm:^4.29.3":
version: 4.29.3
resolution: "@tanstack/react-query-devtools@npm:4.29.3"
dependencies:
"@tanstack/match-sorter-utils": ^8.7.0
superjson: ^1.10.0
use-sync-external-store: ^1.2.0
peerDependencies:
"@tanstack/react-query": 4.29.3
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: fd52531b4ef8776fdb7f6957216a1a933f3507d7e6b20ca63d6a2fe78e25658b6d1a712a71cca04cd29715bc66c2952312434190faedab1b3702cb71527460ee
languageName: node
linkType: hard
"@tanstack/react-query@npm:^4.29.3":
version: 4.29.3
resolution: "@tanstack/react-query@npm:4.29.3"
dependencies:
"@tanstack/query-core": 4.29.1
use-sync-external-store: ^1.2.0
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
react-native: "*"
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
checksum: 7d33fe001ba6ec2332c28749312e5268e6bc060ea0e7c3faa37a28a12fd4b4fddd3803fdfb4e6551997f06d4cd32568e39d9cc9a26c19f40cfef276fad1eaaee
languageName: node
linkType: hard
"@tootallnate/once@npm:2": "@tootallnate/once@npm:2":
version: 2.0.0 version: 2.0.0
resolution: "@tootallnate/once@npm:2.0.0" resolution: "@tootallnate/once@npm:2.0.0"
@ -1234,13 +1284,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"big-integer@npm:^1.6.16":
version: 1.6.51
resolution: "big-integer@npm:1.6.51"
checksum: 3d444173d1b2e20747e2c175568bedeebd8315b0637ea95d75fd27830d3b8e8ba36c6af40374f36bdaea7b5de376dcada1b07587cb2a79a928fccdb6e6e3c518
languageName: node
linkType: hard
"binary-extensions@npm:^2.0.0": "binary-extensions@npm:^2.0.0":
version: 2.2.0 version: 2.2.0
resolution: "binary-extensions@npm:2.2.0" resolution: "binary-extensions@npm:2.2.0"
@ -1276,22 +1319,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"broadcast-channel@npm:^3.4.1":
version: 3.7.0
resolution: "broadcast-channel@npm:3.7.0"
dependencies:
"@babel/runtime": ^7.7.2
detect-node: ^2.1.0
js-sha3: 0.8.0
microseconds: 0.2.0
nano-time: 1.0.0
oblivious-set: 1.0.0
rimraf: 3.0.2
unload: 2.2.0
checksum: 803794c48dcce7f03aca69797430bd8b1c4cfd70b7de22079cd89567eeffaa126a1db98c7c2d86af8131d9bb41ed367c0fef96dfb446151c927b831572c621fc
languageName: node
linkType: hard
"browserslist@npm:^4.21.5": "browserslist@npm:^4.21.5":
version: 4.21.5 version: 4.21.5
resolution: "browserslist@npm:4.21.5" resolution: "browserslist@npm:4.21.5"
@ -1500,6 +1527,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"copy-anything@npm:^3.0.2":
version: 3.0.3
resolution: "copy-anything@npm:3.0.3"
dependencies:
is-what: ^4.1.8
checksum: d456dc5ec98dee7c7cf87d809eac30dc2ac942acd4cf970fab394e280ceb6dd7a8a7a5a44fcbcc50e0206658de3cc20b92863562f5797930bb2619f164f4c182
languageName: node
linkType: hard
"cosmiconfig@npm:^7.0.0": "cosmiconfig@npm:^7.0.0":
version: 7.1.0 version: 7.1.0
resolution: "cosmiconfig@npm:7.1.0" resolution: "cosmiconfig@npm:7.1.0"
@ -1613,13 +1649,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"detect-node@npm:^2.0.4, detect-node@npm:^2.1.0":
version: 2.1.0
resolution: "detect-node@npm:2.1.0"
checksum: 832184ec458353e41533ac9c622f16c19f7c02d8b10c303dfd3a756f56be93e903616c0bb2d4226183c9351c15fc0b3dba41a17a2308262afabcfa3776e6ae6e
languageName: node
linkType: hard
"didyoumean@npm:^1.2.2": "didyoumean@npm:^1.2.2":
version: 1.2.2 version: 1.2.2
resolution: "didyoumean@npm:1.2.2" resolution: "didyoumean@npm:1.2.2"
@ -2956,6 +2985,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"is-what@npm:^4.1.8":
version: 4.1.8
resolution: "is-what@npm:4.1.8"
checksum: b9bec3acff102d14ad467f4c74c9886af310fa160e07a63292c8c181e6768c7c4c1054644e13d67185b963644e4a513bce8c6b8ce3d3ca6f9488a69fccad5f97
languageName: node
linkType: hard
"isexe@npm:^2.0.0": "isexe@npm:^2.0.0":
version: 2.0.0 version: 2.0.0
resolution: "isexe@npm:2.0.0" resolution: "isexe@npm:2.0.0"
@ -2979,13 +3015,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"js-sha3@npm:0.8.0":
version: 0.8.0
resolution: "js-sha3@npm:0.8.0"
checksum: 75df77c1fc266973f06cce8309ce010e9e9f07ec35ab12022ed29b7f0d9c8757f5a73e1b35aa24840dced0dea7059085aa143d817aea9e188e2a80d569d9adce
languageName: node
linkType: hard
"js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0":
version: 4.0.0 version: 4.0.0
resolution: "js-tokens@npm:4.0.0" resolution: "js-tokens@npm:4.0.0"
@ -3193,16 +3222,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"match-sorter@npm:^6.0.2":
version: 6.3.1
resolution: "match-sorter@npm:6.3.1"
dependencies:
"@babel/runtime": ^7.12.5
remove-accents: 0.4.2
checksum: a4b02b676ac4ce64a89a091539ee4a70a802684713bcf06f2b70787927f510fe8a2adc849f9288857a90906083ad303467e530e8723b4a9756df9994fc164550
languageName: node
linkType: hard
"memoize-one@npm:^6.0.0": "memoize-one@npm:^6.0.0":
version: 6.0.0 version: 6.0.0
resolution: "memoize-one@npm:6.0.0" resolution: "memoize-one@npm:6.0.0"
@ -3234,13 +3253,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"microseconds@npm:0.2.0":
version: 0.2.0
resolution: "microseconds@npm:0.2.0"
checksum: 22bfa8553f92c7d95afff6de0aeb2aecf750680d41b8c72b02098ccc5bbbb0a384380ff539292dbd3788f5dfc298682f9d38a2b4c101f5ee2c9471d53934c5fa
languageName: node
linkType: hard
"mimic-fn@npm:^2.1.0": "mimic-fn@npm:^2.1.0":
version: 2.1.0 version: 2.1.0
resolution: "mimic-fn@npm:2.1.0" resolution: "mimic-fn@npm:2.1.0"
@ -3393,15 +3405,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"nano-time@npm:1.0.0":
version: 1.0.0
resolution: "nano-time@npm:1.0.0"
dependencies:
big-integer: ^1.6.16
checksum: eef8548546cc1020625f8e44751a7263e9eddf0412a6a1a6c80a8d2be2ea7973622804a977cdfe796807b85b20ff6c8ba340e8dd20effcc7078193ed5edbb5d4
languageName: node
linkType: hard
"nanoid@npm:^3.3.6": "nanoid@npm:^3.3.6":
version: 3.3.6 version: 3.3.6
resolution: "nanoid@npm:3.3.6" resolution: "nanoid@npm:3.3.6"
@ -3588,13 +3591,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"oblivious-set@npm:1.0.0":
version: 1.0.0
resolution: "oblivious-set@npm:1.0.0"
checksum: f31740ea9c3a8242ad2324e4ebb9a35359fbc2e6e7131731a0fc1c8b7b1238eb07e4c8c631a38535243a7b8e3042b7e89f7dc2a95d2989afd6f80bd5793b0aab
languageName: node
linkType: hard
"once@npm:^1.3.0": "once@npm:^1.3.0":
version: 1.4.0 version: 1.4.0
resolution: "once@npm:1.4.0" resolution: "once@npm:1.4.0"
@ -4009,24 +4005,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-query@npm:^3.39.1":
version: 3.39.3
resolution: "react-query@npm:3.39.3"
dependencies:
"@babel/runtime": ^7.5.5
broadcast-channel: ^3.4.1
match-sorter: ^6.0.2
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
checksum: d2de6a0992dbf039ff2de564de1ae6361f8ac7310159dae42ec16f833b79c05caedced187235c42373ac331cc5f2fe9e2b31b14ae75a815e86d86e30ca9887ad
languageName: node
linkType: hard
"react-ridge-state@npm:4.2.2": "react-ridge-state@npm:4.2.2":
version: 4.2.2 version: 4.2.2
resolution: "react-ridge-state@npm:4.2.2" resolution: "react-ridge-state@npm:4.2.2"
@ -4260,7 +4238,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"rimraf@npm:3.0.2, rimraf@npm:^3.0.2": "rimraf@npm:^3.0.2":
version: 3.0.2 version: 3.0.2
resolution: "rimraf@npm:3.0.2" resolution: "rimraf@npm:3.0.2"
dependencies: dependencies:
@ -4588,6 +4566,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"superjson@npm:^1.10.0":
version: 1.12.3
resolution: "superjson@npm:1.12.3"
dependencies:
copy-anything: ^3.0.2
checksum: 3549cc1d03e93745632d8114f91ed1668d81a0cf4c618f8f89a1b06f426a9cd1a2879f0e79469a6a193fd19dcea9a8fecff6215d12527b98c40c67cd98f185d3
languageName: node
linkType: hard
"supports-color@npm:^5.3.0": "supports-color@npm:^5.3.0":
version: 5.5.0 version: 5.5.0
resolution: "supports-color@npm:5.5.0" resolution: "supports-color@npm:5.5.0"
@ -4826,16 +4813,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"unload@npm:2.2.0":
version: 2.2.0
resolution: "unload@npm:2.2.0"
dependencies:
"@babel/runtime": ^7.6.2
detect-node: ^2.0.4
checksum: 88ba950c5ff83ab4f9bbd8f63bbf19ba09687ed3c434efd43b7338cc595bc574df8f9b155ee6eee7a435de3d3a4a226726988428977a68ba4907045f1fac5d41
languageName: node
linkType: hard
"update-browserslist-db@npm:^1.0.10": "update-browserslist-db@npm:^1.0.10":
version: 1.0.11 version: 1.0.11
resolution: "update-browserslist-db@npm:1.0.11" resolution: "update-browserslist-db@npm:1.0.11"
@ -4871,6 +4848,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"use-sync-external-store@npm:^1.2.0":
version: 1.2.0
resolution: "use-sync-external-store@npm:1.2.0"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: 5c639e0f8da3521d605f59ce5be9e094ca772bd44a4ce7322b055a6f58eeed8dda3c94cabd90c7a41fb6fa852210092008afe48f7038792fd47501f33299116a
languageName: node
linkType: hard
"util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2": "util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2":
version: 1.0.2 version: 1.0.2
resolution: "util-deprecate@npm:1.0.2" resolution: "util-deprecate@npm:1.0.2"
@ -4934,6 +4920,8 @@ __metadata:
"@heroicons/react": ^2.0.11 "@heroicons/react": ^2.0.11
"@hookform/error-message": ^2.0.0 "@hookform/error-message": ^2.0.0
"@tailwindcss/forms": ^0.5.2 "@tailwindcss/forms": ^0.5.2
"@tanstack/react-query": ^4.29.3
"@tanstack/react-query-devtools": ^4.29.3
"@types/node": ^18.0.0 "@types/node": ^18.0.0
"@types/react": ^18.0.12 "@types/react": ^18.0.12
"@types/react-dom": ^18.0.5 "@types/react-dom": ^18.0.5
@ -4962,7 +4950,6 @@ __metadata:
react-multi-select-component: ^4.2.9 react-multi-select-component: ^4.2.9
react-popper-tooltip: ^4.4.2 react-popper-tooltip: ^4.4.2
react-portal: ^4.2.2 react-portal: ^4.2.2
react-query: ^3.39.1
react-ridge-state: 4.2.2 react-ridge-state: 4.2.2
react-router-dom: ^6.3.0 react-router-dom: ^6.3.0
react-select: ^5.3.2 react-select: ^5.3.2