mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +00:00
refactor(web) add eslint (#222)
* fix(tsconfig.json): changed skipLibCheck to false. refactor(eslint): moved configuration from package.json to .eslintrc.js and added a typescript plugin for future use * feat: wip eslint and types * feat: fix identation * feat: get rid of last any types
This commit is contained in:
parent
7f06a4c707
commit
cb8f280e86
70 changed files with 6797 additions and 6541 deletions
71
web/.eslintrc.js
Normal file
71
web/.eslintrc.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: [
|
||||
"@typescript-eslint",
|
||||
],
|
||||
// If we ever decide on a code-style, I'll leave this here.
|
||||
//extends: [
|
||||
// "airbnb",
|
||||
// "airbnb/hooks",
|
||||
// "airbnb-typescript",
|
||||
//],
|
||||
rules: {
|
||||
// Turn off pesky "react not in scope" error while
|
||||
// we transition to proper ESLint support
|
||||
"react/react-in-jsx-scope": "off",
|
||||
// Add a UNIX-style linebreak at the end of each file
|
||||
"linebreak-style": ["error", "unix"],
|
||||
// Allow only double quotes and backticks
|
||||
quotes: ["error", "double"],
|
||||
// Warn if a line isn't indented with a multiple of 2
|
||||
indent: ["warn", 2],
|
||||
// Don't enforce any particular brace style
|
||||
curly: "off",
|
||||
// Let's keep these off for now and
|
||||
// maybe turn these back on sometime in the future
|
||||
"import/prefer-default-export": "off",
|
||||
"react/function-component-definition": "off",
|
||||
"nonblock-statement-body-position": ["warn", "below"]
|
||||
},
|
||||
// Conditionally run the following configuration only for TS files.
|
||||
// Otherwise, this will create inter-op problems with JS files.
|
||||
overrides: [
|
||||
{
|
||||
// Run only .ts and .tsx files
|
||||
files: ["*.ts", "*.tsx"],
|
||||
// Define the @typescript-eslint plugin schemas
|
||||
extends: [
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
// Don't require strict type-checking for now, since we have too many
|
||||
// dubious statements literred in the code.
|
||||
//"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||
],
|
||||
parserOptions: {
|
||||
project: "tsconfig.json",
|
||||
// This is needed so we can always point to the tsconfig.json
|
||||
// file relative to the current .eslintrc.js file.
|
||||
// Generally, a problem occurrs when "npm run lint"
|
||||
// gets ran from another directory. This fixes it.
|
||||
tsconfigRootDir: __dirname,
|
||||
sourceType: "module",
|
||||
},
|
||||
// Override JS rules and apply @typescript-eslint rules
|
||||
// as they might interfere with eachother.
|
||||
rules: {
|
||||
quotes: "off",
|
||||
"@typescript-eslint/quotes": ["error", "double"],
|
||||
semi: "off",
|
||||
"@typescript-eslint/semi": ["warn", "always"],
|
||||
// indent: "off",
|
||||
indent: ["warn", 2],
|
||||
"@typescript-eslint/indent": "off",
|
||||
"@typescript-eslint/comma-dangle": "warn",
|
||||
"keyword-spacing": "off",
|
||||
"@typescript-eslint/keyword-spacing": ["error"],
|
||||
"object-curly-spacing": "off",
|
||||
"@typescript-eslint/object-curly-spacing": ["warn", "always"],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
|
@ -29,7 +29,7 @@
|
|||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"lint": "esw src/ --ext \".ts,.tsx,.js,.jsx\" --color",
|
||||
"lint": "eslint src/ --ext .js,.jsx,.ts,.tsx --color",
|
||||
"lint:watch": "npm run lint -- --watch"
|
||||
},
|
||||
"browserslist": {
|
||||
|
@ -59,8 +59,8 @@
|
|||
"@types/react-dom": "17.0.0",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"@types/react-table": "^7.7.7",
|
||||
"@typescript-eslint/eslint-plugin": "^5.10.2",
|
||||
"@typescript-eslint/parser": "^5.10.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.18.0",
|
||||
"@typescript-eslint/parser": "^5.18.0",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"eslint": "^8.8.0",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
|
@ -71,59 +71,5 @@
|
|||
"postcss": "^8.4.6",
|
||||
"tailwindcss": "^3.0.18",
|
||||
"typescript": "^4.1.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:import/errors",
|
||||
"plugin:import/warnings",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react-hooks/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"react",
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"react/jsx-uses-react": "off",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off"
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 11,
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true,
|
||||
"experimentalObjectRestSpread": true
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
},
|
||||
"import/resolver": {
|
||||
"node": {
|
||||
"extensions": [
|
||||
".ts",
|
||||
".tsx",
|
||||
".js",
|
||||
".jsx"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"jquery": false
|
||||
},
|
||||
"globals": {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import Toast from "./components/notifications/Toast";
|
|||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { useErrorBoundary: true, },
|
||||
queries: { useErrorBoundary: true },
|
||||
mutations: {
|
||||
onError: (error) => {
|
||||
// Use a format string to convert the error object to a proper string without much hassle.
|
||||
|
@ -27,8 +27,8 @@ export const queryClient = new QueryClient({
|
|||
);
|
||||
toast.custom((t) => <Toast type="error" body={message} t={t} />);
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export function App() {
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import {baseUrl, sseBaseUrl} from "../utils";
|
||||
import {AuthContext} from "../utils/Context";
|
||||
import {Cookies} from "react-cookie";
|
||||
import { baseUrl, sseBaseUrl } from "../utils";
|
||||
import { AuthContext } from "../utils/Context";
|
||||
import { Cookies } from "react-cookie";
|
||||
|
||||
interface ConfigType {
|
||||
body?: BodyInit | Record<string, unknown> | null;
|
||||
body?: BodyInit | Record<string, unknown> | unknown | null;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
type PostBody = BodyInit | Record<string, unknown> | unknown | null;
|
||||
|
||||
export async function HttpClient<T>(
|
||||
endpoint: string,
|
||||
method: string,
|
||||
|
@ -29,7 +31,7 @@ export async function HttpClient<T>(
|
|||
// if 401 consider the session expired and force logout
|
||||
const cookies = new Cookies();
|
||||
cookies.remove("user_session");
|
||||
AuthContext.reset()
|
||||
AuthContext.reset();
|
||||
|
||||
return Promise.reject(new Error(response.statusText));
|
||||
}
|
||||
|
@ -56,25 +58,32 @@ export async function HttpClient<T>(
|
|||
|
||||
const appClient = {
|
||||
Get: <T>(endpoint: string) => HttpClient<T>(endpoint, "GET"),
|
||||
Post: <T>(endpoint: string, data: any) => HttpClient<void | T>(endpoint, "POST", { body: data }),
|
||||
Put: (endpoint: string, data: any) => HttpClient<void>(endpoint, "PUT", { body: data }),
|
||||
Patch: (endpoint: string, data: any) => HttpClient<void>(endpoint, "PATCH", { body: data }),
|
||||
Post: <T>(endpoint: string, data: PostBody) => HttpClient<void | T>(endpoint, "POST", { body: data }),
|
||||
PostBody: <T>(endpoint: string, data: PostBody) => HttpClient<T>(endpoint, "POST", { body: data }),
|
||||
Put: (endpoint: string, data: PostBody) => HttpClient<void>(endpoint, "PUT", { body: data }),
|
||||
Patch: (endpoint: string, data: PostBody) => HttpClient<void>(endpoint, "PATCH", { body: data }),
|
||||
Delete: (endpoint: string) => HttpClient<void>(endpoint, "DELETE")
|
||||
}
|
||||
};
|
||||
|
||||
export const APIClient = {
|
||||
auth: {
|
||||
login: (username: string, password: string) => appClient.Post("api/auth/login", { username: username, password: password }),
|
||||
login: (username: string, password: string) => appClient.Post("api/auth/login", {
|
||||
username: username,
|
||||
password: password
|
||||
}),
|
||||
logout: () => appClient.Post("api/auth/logout", null),
|
||||
validate: () => appClient.Get<void>("api/auth/validate"),
|
||||
onboard: (username: string, password: string) => appClient.Post("api/auth/onboard", { username: username, password: password }),
|
||||
canOnboard: () => appClient.Get("api/auth/onboard"),
|
||||
onboard: (username: string, password: string) => appClient.Post("api/auth/onboard", {
|
||||
username: username,
|
||||
password: password
|
||||
}),
|
||||
canOnboard: () => appClient.Get("api/auth/onboard")
|
||||
},
|
||||
actions: {
|
||||
create: (action: Action) => appClient.Post("api/actions", action),
|
||||
update: (action: Action) => appClient.Put(`api/actions/${action.id}`, action),
|
||||
delete: (id: number) => appClient.Delete(`api/actions/${id}`),
|
||||
toggleEnable: (id: number) => appClient.Patch(`api/actions/${id}/toggleEnabled`, null),
|
||||
toggleEnable: (id: number) => appClient.Patch(`api/actions/${id}/toggleEnabled`, null)
|
||||
},
|
||||
config: {
|
||||
get: () => appClient.Get<Config>("api/config")
|
||||
|
@ -84,7 +93,7 @@ export const APIClient = {
|
|||
create: (dc: DownloadClient) => appClient.Post("api/download_clients", dc),
|
||||
update: (dc: DownloadClient) => appClient.Put("api/download_clients", dc),
|
||||
delete: (id: number) => appClient.Delete(`api/download_clients/${id}`),
|
||||
test: (dc: DownloadClient) => appClient.Post("api/download_clients/test", dc),
|
||||
test: (dc: DownloadClient) => appClient.Post("api/download_clients/test", dc)
|
||||
},
|
||||
filters: {
|
||||
getAll: () => appClient.Get<Filter[]>("api/filters"),
|
||||
|
@ -93,14 +102,14 @@ export const APIClient = {
|
|||
update: (filter: Filter) => appClient.Put(`api/filters/${filter.id}`, filter),
|
||||
duplicate: (id: number) => appClient.Get<Filter>(`api/filters/${id}/duplicate`),
|
||||
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}`)
|
||||
},
|
||||
feeds: {
|
||||
find: () => appClient.Get<Feed[]>("api/feeds"),
|
||||
create: (feed: FeedCreate) => appClient.Post("api/feeds", feed),
|
||||
toggleEnable: (id: number, enabled: boolean) => appClient.Patch(`api/feeds/${id}/enabled`, { enabled }),
|
||||
update: (feed: Feed) => appClient.Put(`api/feeds/${feed.id}`, feed),
|
||||
delete: (id: number) => appClient.Delete(`api/feeds/${id}`),
|
||||
delete: (id: number) => appClient.Delete(`api/feeds/${id}`)
|
||||
},
|
||||
indexers: {
|
||||
// returns indexer options for all currently present/enabled indexers
|
||||
|
@ -109,15 +118,15 @@ export const APIClient = {
|
|||
getAll: () => appClient.Get<IndexerDefinition[]>("api/indexer"),
|
||||
// returns all possible indexer definitions
|
||||
getSchema: () => appClient.Get<IndexerDefinition[]>("api/indexer/schema"),
|
||||
create: (indexer: Indexer) => appClient.Post<Indexer>("api/indexer", indexer),
|
||||
create: (indexer: Indexer) => appClient.PostBody<Indexer>("api/indexer", indexer),
|
||||
update: (indexer: Indexer) => appClient.Put("api/indexer", indexer),
|
||||
delete: (id: number) => appClient.Delete(`api/indexer/${id}`),
|
||||
delete: (id: number) => appClient.Delete(`api/indexer/${id}`)
|
||||
},
|
||||
irc: {
|
||||
getNetworks: () => appClient.Get<IrcNetworkWithHealth[]>("api/irc"),
|
||||
createNetwork: (network: IrcNetworkCreate) => appClient.Post("api/irc", network),
|
||||
updateNetwork: (network: IrcNetwork) => appClient.Put(`api/irc/network/${network.id}`, network),
|
||||
deleteNetwork: (id: number) => appClient.Delete(`api/irc/network/${id}`),
|
||||
deleteNetwork: (id: number) => appClient.Delete(`api/irc/network/${id}`)
|
||||
},
|
||||
events: {
|
||||
logs: () => new EventSource(`${sseBaseUrl()}api/events?stream=logs`, { withCredentials: true })
|
||||
|
@ -126,7 +135,7 @@ export const APIClient = {
|
|||
getAll: () => appClient.Get<Notification[]>("api/notification"),
|
||||
create: (notification: Notification) => appClient.Post("api/notification", notification),
|
||||
update: (notification: Notification) => appClient.Put(`api/notification/${notification.id}`, notification),
|
||||
delete: (id: number) => appClient.Delete(`api/notification/${id}`),
|
||||
delete: (id: number) => appClient.Delete(`api/notification/${id}`)
|
||||
},
|
||||
release: {
|
||||
find: (query?: string) => appClient.Get<ReleaseFindResponse>(`api/release${query}`),
|
||||
|
@ -148,10 +157,10 @@ export const APIClient = {
|
|||
params.append("push_status", filter.value);
|
||||
});
|
||||
|
||||
return appClient.Get<ReleaseFindResponse>(`api/release?${params.toString()}`)
|
||||
return appClient.Get<ReleaseFindResponse>(`api/release?${params.toString()}`);
|
||||
},
|
||||
indexerOptions: () => appClient.Get<string[]>(`api/release/indexers`),
|
||||
indexerOptions: () => appClient.Get<string[]>("api/release/indexers"),
|
||||
stats: () => appClient.Get<ReleaseStats>("api/release/stats"),
|
||||
delete: () => appClient.Delete(`api/release/all`),
|
||||
delete: () => appClient.Delete("api/release/all")
|
||||
}
|
||||
};
|
|
@ -23,11 +23,11 @@ export const Checkbox = ({ label, description, value, setValue }: CheckboxProps)
|
|||
checked={value}
|
||||
onChange={setValue}
|
||||
className={
|
||||
`${value ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-700'
|
||||
`${value ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-700"
|
||||
} ml-4 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`}
|
||||
>
|
||||
<span
|
||||
className={`${value ? 'translate-x-5' : 'translate-x-0'} inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200`}
|
||||
className={`${value ? "translate-x-5" : "translate-x-0"} inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200`}
|
||||
/>
|
||||
</Switch>
|
||||
</Switch.Group>
|
||||
|
|
|
@ -51,7 +51,8 @@ export const ErrorPage = ({ error, resetErrorBoundary }: FallbackProps) => {
|
|||
role="alert"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<svg className="mr-2 w-5 h-5 text-red-700 dark:text-red-800" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg className="mr-2 w-5 h-5 text-red-700 dark:text-red-800" fill="currentColor" viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
|
||||
|
@ -77,11 +78,11 @@ export const ErrorPage = ({ error, resetErrorBoundary }: FallbackProps) => {
|
|||
resetErrorBoundary();
|
||||
}}
|
||||
>
|
||||
<RefreshIcon className="-ml-0.5 mr-2 h-5 w-5" />
|
||||
<RefreshIcon className="-ml-0.5 mr-2 h-5 w-5"/>
|
||||
Reset page state
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -1,33 +1,36 @@
|
|||
import { classNames } from "../../utils"
|
||||
import React from "react";
|
||||
import { classNames } from "../../utils";
|
||||
|
||||
interface ButtonProps {
|
||||
className?: string;
|
||||
children: any;
|
||||
[rest: string]: any;
|
||||
children: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const Button = ({ children, className, ...rest }: ButtonProps) => (
|
||||
export const Button = ({ children, className, disabled, onClick }: ButtonProps) => (
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(
|
||||
className ?? "",
|
||||
"relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-800 text-sm font-medium rounded-md text-gray-700 dark:text-gray-500 bg-white dark:bg-gray-800 hover:bg-gray-50"
|
||||
)}
|
||||
{...rest}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
|
||||
|
||||
export const PageButton = ({ children, className, ...rest }: ButtonProps) => (
|
||||
export const PageButton = ({ children, className, disabled, onClick }: ButtonProps) => (
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(
|
||||
className ?? "",
|
||||
"relative inline-flex items-center px-2 py-2 border border-gray-300 dark:border-gray-700 text-sm font-medium text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-600"
|
||||
)}
|
||||
{...rest}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
|
|
|
@ -26,8 +26,6 @@ export const TitleCell = ({ value }: CellProps) => (
|
|||
|
||||
interface ReleaseStatusCellProps {
|
||||
value: ReleaseActionStatus[];
|
||||
column: any;
|
||||
row: any;
|
||||
}
|
||||
|
||||
interface StatusCellMapEntry {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { FC } from "react"
|
||||
import { FC } from "react";
|
||||
|
||||
interface DebugProps {
|
||||
values: unknown;
|
||||
|
@ -10,10 +10,8 @@ const DEBUG: FC<DebugProps> = ({ values }) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="w-full p-2 flex flex-col mt-6 bg-gray-100 dark:bg-gray-900">
|
||||
<pre className="overflow-x-auto dark:text-gray-400">
|
||||
{JSON.stringify(values, undefined, 2)}
|
||||
</pre>
|
||||
<div className="w-full p-2 flex flex-col mt-12 mb-12 bg-gray-100 dark:bg-gray-900">
|
||||
<pre className="dark:text-gray-400">{JSON.stringify(values, null, 2)}</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -29,12 +29,12 @@ export const EmptySimple = ({
|
|||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
interface EmptyListStateProps {
|
||||
text: string;
|
||||
buttonText?: string;
|
||||
buttonOnClick?: any;
|
||||
buttonOnClick?: () => void;
|
||||
}
|
||||
|
||||
export function EmptyListState({ text, buttonText, buttonOnClick }: EmptyListStateProps) {
|
||||
|
@ -51,5 +51,5 @@ export function EmptyListState({ text, buttonText, buttonOnClick }: EmptyListSta
|
|||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
|
@ -10,4 +10,4 @@ export const TitleSubtitle: FC<Props> = ({ title, subtitle }) => (
|
|||
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">{title}</h2>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{subtitle}</p>
|
||||
</div>
|
||||
)
|
||||
);
|
|
@ -1,14 +1,13 @@
|
|||
import { Field } from "formik";
|
||||
import { Field, FieldProps } from "formik";
|
||||
|
||||
interface ErrorFieldProps {
|
||||
name: string;
|
||||
classNames?: string;
|
||||
subscribe?: any;
|
||||
}
|
||||
|
||||
const ErrorField = ({ name, classNames }: ErrorFieldProps) => (
|
||||
<Field name={name} subscribe={{ touched: true, error: true }}>
|
||||
{({ meta: { touched, error } }: any) =>
|
||||
{({ meta: { touched, error } }: FieldProps) =>
|
||||
touched && error ? <span className={classNames}>{error}</span> : null
|
||||
}
|
||||
</Field>
|
||||
|
@ -41,6 +40,6 @@ const CheckboxField = ({
|
|||
<p className="text-gray-500">{sublabel}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
export { ErrorField, CheckboxField }
|
||||
export { ErrorField, CheckboxField };
|
|
@ -2,6 +2,6 @@ export { ErrorField, CheckboxField } from "./common";
|
|||
export { TextField, NumberField, PasswordField } from "./input";
|
||||
export { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "./input_wide";
|
||||
export { RadioFieldsetWide } from "./radio";
|
||||
export { MultiSelect, Select, SelectWide, DownloadClientSelect, IndexerMultiSelect} from "./select";
|
||||
export { MultiSelect, Select, SelectWide, DownloadClientSelect, IndexerMultiSelect } from "./select";
|
||||
export { SwitchGroup } from "./switch";
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Field } from "formik";
|
||||
import { Field, FieldProps } from "formik";
|
||||
import { classNames } from "../../utils";
|
||||
import { EyeIcon, EyeOffIcon } from "@heroicons/react/solid";
|
||||
import { useToggle } from "../../hooks/hooks";
|
||||
|
@ -22,12 +22,12 @@ export const TextField = ({
|
|||
placeholder,
|
||||
columns,
|
||||
autoComplete,
|
||||
hidden,
|
||||
hidden
|
||||
}: TextFieldProps) => (
|
||||
<div
|
||||
className={classNames(
|
||||
hidden ? "hidden" : "",
|
||||
columns ? `col-span-${columns}` : "col-span-12",
|
||||
columns ? `col-span-${columns}` : "col-span-12"
|
||||
)}
|
||||
>
|
||||
{label && (
|
||||
|
@ -38,8 +38,8 @@ export const TextField = ({
|
|||
<Field name={name}>
|
||||
{({
|
||||
field,
|
||||
meta,
|
||||
}: any) => (
|
||||
meta
|
||||
}: FieldProps) => (
|
||||
<div>
|
||||
<input
|
||||
{...field}
|
||||
|
@ -58,7 +58,7 @@ export const TextField = ({
|
|||
)}
|
||||
</Field>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
interface PasswordFieldProps {
|
||||
name: string;
|
||||
|
@ -81,7 +81,7 @@ export const PasswordField = ({
|
|||
help,
|
||||
required
|
||||
}: PasswordFieldProps) => {
|
||||
const [isVisible, toggleVisibility] = useToggle(false)
|
||||
const [isVisible, toggleVisibility] = useToggle(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -97,8 +97,8 @@ export const PasswordField = ({
|
|||
<Field name={name} defaultValue={defaultValue}>
|
||||
{({
|
||||
field,
|
||||
meta,
|
||||
}: any) => (
|
||||
meta
|
||||
}: FieldProps) => (
|
||||
<div className="sm:col-span-2 relative">
|
||||
<input
|
||||
{...field}
|
||||
|
@ -124,8 +124,8 @@ export const PasswordField = ({
|
|||
)}
|
||||
</Field>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
interface NumberFieldProps {
|
||||
name: string;
|
||||
|
@ -138,7 +138,7 @@ export const NumberField = ({
|
|||
name,
|
||||
label,
|
||||
placeholder,
|
||||
step,
|
||||
step
|
||||
}: NumberFieldProps) => (
|
||||
<div className="col-span-12 sm:col-span-6">
|
||||
<label htmlFor={name} className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||
|
@ -148,8 +148,8 @@ export const NumberField = ({
|
|||
<Field name={name} type="number">
|
||||
{({
|
||||
field,
|
||||
meta,
|
||||
}: any) => (
|
||||
meta
|
||||
}: FieldProps) => (
|
||||
<div className="sm:col-span-2">
|
||||
<input
|
||||
type="number"
|
||||
|
|
|
@ -4,7 +4,7 @@ import { classNames } from "../../utils";
|
|||
import { useToggle } from "../../hooks/hooks";
|
||||
import { EyeIcon, EyeOffIcon } from "@heroicons/react/solid";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import { ErrorField } from "./common"
|
||||
import { ErrorField } from "./common";
|
||||
|
||||
interface TextFieldWideProps {
|
||||
name: string;
|
||||
|
@ -56,7 +56,7 @@ export const TextFieldWide = ({
|
|||
<ErrorField name={name} classNames="block text-red-500 mt-2" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
interface PasswordFieldWideProps {
|
||||
name: string;
|
||||
|
@ -77,7 +77,7 @@ export const PasswordFieldWide = ({
|
|||
required,
|
||||
defaultVisible
|
||||
}: PasswordFieldWideProps) => {
|
||||
const [isVisible, toggleVisibility] = useToggle(defaultVisible)
|
||||
const [isVisible, toggleVisibility] = useToggle(defaultVisible);
|
||||
|
||||
return (
|
||||
<div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
||||
|
@ -114,8 +114,8 @@ export const PasswordFieldWide = ({
|
|||
<ErrorField name={name} classNames="block text-red-500 mt-2" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
interface NumberFieldWideProps {
|
||||
name: string;
|
||||
|
@ -154,7 +154,7 @@ export const NumberFieldWide = ({
|
|||
id={name}
|
||||
type="number"
|
||||
value={field.value ? field.value : defaultValue ?? 0}
|
||||
onChange={(e) => { form.setFieldValue(field.name, parseInt(e.target.value)) }}
|
||||
onChange={(e) => { form.setFieldValue(field.name, parseInt(e.target.value)); }}
|
||||
className={classNames(
|
||||
meta.touched && meta.error
|
||||
? "focus:ring-red-500 focus:border-red-500 border-red-500"
|
||||
|
@ -213,19 +213,19 @@ export const SwitchGroupWide = ({
|
|||
value={field.value}
|
||||
checked={field.checked ?? false}
|
||||
onChange={value => {
|
||||
form.setFieldValue(field?.name ?? '', value)
|
||||
form.setFieldValue(field?.name ?? "", value);
|
||||
}}
|
||||
className={classNames(
|
||||
field.value ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-500',
|
||||
'ml-4 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'
|
||||
field.value ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-500",
|
||||
"ml-4 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"
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
field.value ? 'translate-x-5' : 'translate-x-0',
|
||||
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
||||
field.value ? "translate-x-5" : "translate-x-0",
|
||||
"inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
|
@ -233,5 +233,5 @@ export const SwitchGroupWide = ({
|
|||
</Field>
|
||||
</Switch.Group>
|
||||
</ul>
|
||||
)
|
||||
);
|
||||
|
||||
|
|
|
@ -15,15 +15,20 @@ interface props {
|
|||
options: radioFieldsetOption[];
|
||||
}
|
||||
|
||||
interface anyObj {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
function RadioFieldsetWide({ name, legend, options }: props) {
|
||||
const {
|
||||
values,
|
||||
setFieldValue,
|
||||
} = useFormikContext<any>();
|
||||
setFieldValue
|
||||
} = useFormikContext<anyObj>();
|
||||
|
||||
|
||||
const onChange = (value: string) => {
|
||||
setFieldValue(name, value)
|
||||
}
|
||||
setFieldValue(name, value);
|
||||
};
|
||||
|
||||
return (
|
||||
<fieldset>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Fragment } from "react";
|
||||
import { Field } from "formik";
|
||||
import { Field, FieldProps } from "formik";
|
||||
import { Transition, Listbox } from "@headlessui/react";
|
||||
import { CheckIcon, SelectorIcon } from "@heroicons/react/solid";
|
||||
import { MultiSelect as RMSC } from "react-multi-select-component";
|
||||
|
@ -7,10 +7,17 @@ import { MultiSelect as RMSC } from "react-multi-select-component";
|
|||
import { classNames, COL_WIDTHS } from "../../utils";
|
||||
import { SettingsContext } from "../../utils/Context";
|
||||
|
||||
export interface MultiSelectOption {
|
||||
value: string | number;
|
||||
label: string;
|
||||
key?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface MultiSelectProps {
|
||||
name: string;
|
||||
label?: string;
|
||||
options?: [] | any;
|
||||
options: MultiSelectOption[];
|
||||
columns?: COL_WIDTHS;
|
||||
creatable?: boolean;
|
||||
}
|
||||
|
@ -20,14 +27,14 @@ export const MultiSelect = ({
|
|||
label,
|
||||
options,
|
||||
columns,
|
||||
creatable,
|
||||
creatable
|
||||
}: MultiSelectProps) => {
|
||||
const settingsContext = SettingsContext.useValue();
|
||||
|
||||
const handleNewField = (value: string) => ({
|
||||
value: value.toUpperCase(),
|
||||
label: value.toUpperCase(),
|
||||
key: value,
|
||||
key: value
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -46,21 +53,20 @@ export const MultiSelect = ({
|
|||
<Field name={name} type="select" multiple={true}>
|
||||
{({
|
||||
field,
|
||||
form: { setFieldValue },
|
||||
}: any) => (
|
||||
form: { setFieldValue }
|
||||
}: FieldProps) => (
|
||||
<RMSC
|
||||
{...field}
|
||||
type="select"
|
||||
options={[...[...options, ...field.value.map((i: any) => ({ value: i.value ?? i, label: i.label ?? i}))].reduce((map, obj) => map.set(obj.value, obj), new Map()).values()]}
|
||||
options={[...[...options, ...field.value.map((i: MultiSelectOption) => ({ value: i.value ?? i, label: i.label ?? i }))].reduce((map, obj) => map.set(obj.value, obj), new Map()).values()]}
|
||||
labelledBy={name}
|
||||
isCreatable={creatable}
|
||||
onCreateOption={handleNewField}
|
||||
value={field.value && field.value.map((item: any) => ({
|
||||
value={field.value && field.value.map((item: MultiSelectOption) => ({
|
||||
value: item.value ? item.value : item,
|
||||
label: item.label ? item.label : item,
|
||||
label: item.label ? item.label : item
|
||||
}))}
|
||||
onChange={(values: any) => {
|
||||
const am = values && values.map((i: any) => i.value);
|
||||
onChange={(values: Array<MultiSelectOption>) => {
|
||||
const am = values && values.map((i) => i.value);
|
||||
|
||||
setFieldValue(field.name, am);
|
||||
}}
|
||||
|
@ -70,13 +76,18 @@ export const MultiSelect = ({
|
|||
</Field>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface IndexerMultiSelectOption {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const IndexerMultiSelect = ({
|
||||
name,
|
||||
label,
|
||||
options,
|
||||
columns,
|
||||
columns
|
||||
}: MultiSelectProps) => {
|
||||
const settingsContext = SettingsContext.useValue();
|
||||
return (
|
||||
|
@ -95,26 +106,26 @@ export const IndexerMultiSelect = ({
|
|||
<Field name={name} type="select" multiple={true}>
|
||||
{({
|
||||
field,
|
||||
form: { setFieldValue },
|
||||
}: any) => (
|
||||
form: { setFieldValue }
|
||||
}: FieldProps) => (
|
||||
<RMSC
|
||||
{...field}
|
||||
type="select"
|
||||
options={options}
|
||||
labelledBy={name}
|
||||
value={field.value && field.value.map((item: any) => options.find((o: any) => o.value?.id === item.id))}
|
||||
onChange={(values: any) => {
|
||||
const am = values && values.map((i: any) => i.value);
|
||||
setFieldValue(field.name, am);
|
||||
value={field.value && field.value.map((item: IndexerMultiSelectOption) => ({
|
||||
value: item.id, label: item.name
|
||||
}))}
|
||||
onChange={(values: MultiSelectOption[]) => {
|
||||
const item = values && values.map((i) => ({ id: i.value, name: i.label }));
|
||||
setFieldValue(field.name, item);
|
||||
}}
|
||||
className={settingsContext.darkTheme ? "dark" : ""}
|
||||
itemHeight={50}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
interface DownloadClientSelectProps {
|
||||
name: string;
|
||||
|
@ -132,11 +143,11 @@ export function DownloadClientSelect({
|
|||
<Field name={name} type="select">
|
||||
{({
|
||||
field,
|
||||
form: { setFieldValue },
|
||||
}: any) => (
|
||||
form: { setFieldValue }
|
||||
}: FieldProps) => (
|
||||
<Listbox
|
||||
value={field.value}
|
||||
onChange={(value: any) => setFieldValue(field?.name, value)}
|
||||
onChange={(value) => setFieldValue(field?.name, value)}
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
|
@ -147,7 +158,7 @@ export function DownloadClientSelect({
|
|||
<Listbox.Button className="bg-white dark:bg-gray-800 relative w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 dark:text-gray-200 sm:text-sm">
|
||||
<span className="block truncate">
|
||||
{field.value
|
||||
? clients.find((c) => c.id === field.value)!.name
|
||||
? clients.find((c) => c.id === field.value)?.name
|
||||
: "Choose a client"}
|
||||
</span>
|
||||
{/*<span className="block truncate">Choose a client</span>*/}
|
||||
|
@ -171,7 +182,7 @@ export function DownloadClientSelect({
|
|||
>
|
||||
{clients
|
||||
.filter((c) => c.type === action.type)
|
||||
.map((client: any) => (
|
||||
.map((client) => (
|
||||
<Listbox.Option
|
||||
key={client.id}
|
||||
className={({ active }) => classNames(
|
||||
|
@ -244,11 +255,11 @@ export const Select = ({
|
|||
<Field name={name} type="select">
|
||||
{({
|
||||
field,
|
||||
form: { setFieldValue },
|
||||
}: any) => (
|
||||
form: { setFieldValue }
|
||||
}: FieldProps) => (
|
||||
<Listbox
|
||||
value={field.value}
|
||||
onChange={(value: any) => setFieldValue(field?.name, value)}
|
||||
onChange={(value) => setFieldValue(field?.name, value)}
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
|
@ -259,7 +270,7 @@ export const Select = ({
|
|||
<Listbox.Button className="bg-white dark:bg-gray-800 relative w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 dark:text-gray-200 sm:text-sm">
|
||||
<span className="block truncate">
|
||||
{field.value
|
||||
? options.find((c) => c.value === field.value)!.label
|
||||
? options.find((c) => c.value === field.value)?.label
|
||||
: optionDefaultText
|
||||
}
|
||||
</span>
|
||||
|
@ -333,7 +344,7 @@ export const Select = ({
|
|||
</Field>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const SelectWide = ({
|
||||
name,
|
||||
|
@ -348,11 +359,11 @@ export const SelectWide = ({
|
|||
<Field name={name} type="select">
|
||||
{({
|
||||
field,
|
||||
form: { setFieldValue },
|
||||
}: any) => (
|
||||
form: { setFieldValue }
|
||||
}: FieldProps) => (
|
||||
<Listbox
|
||||
value={field.value}
|
||||
onChange={(value: any) => setFieldValue(field?.name, value)}
|
||||
onChange={(value) => setFieldValue(field?.name, value)}
|
||||
>
|
||||
{({ open }) => (
|
||||
<div className="py-4 flex items-center justify-between">
|
||||
|
@ -364,7 +375,7 @@ export const SelectWide = ({
|
|||
<Listbox.Button className="bg-white dark:bg-gray-800 relative w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 dark:text-gray-200 sm:text-sm">
|
||||
<span className="block truncate">
|
||||
{field.value
|
||||
? options.find((c) => c.value === field.value)!.label
|
||||
? options.find((c) => c.value === field.value)?.label
|
||||
: optionDefaultText
|
||||
}
|
||||
</span>
|
||||
|
@ -439,4 +450,4 @@ export const SelectWide = ({
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import React from "react";
|
||||
import { Field } from "formik";
|
||||
import type {
|
||||
FieldInputProps,
|
||||
|
@ -9,15 +10,18 @@ import type {
|
|||
import { Switch as HeadlessSwitch } from "@headlessui/react";
|
||||
import { classNames } from "../../utils";
|
||||
|
||||
type SwitchProps<V = any> = {
|
||||
label: string
|
||||
type SwitchProps<V = unknown> = {
|
||||
label?: string
|
||||
checked: boolean
|
||||
value: boolean
|
||||
disabled?: boolean
|
||||
onChange: (value: boolean) => void
|
||||
field?: FieldInputProps<V>
|
||||
form?: FormikProps<FormikValues>
|
||||
meta?: FieldMetaProps<V>
|
||||
}
|
||||
children: React.ReactNode
|
||||
className: string
|
||||
};
|
||||
|
||||
export const Switch = ({
|
||||
label,
|
||||
|
@ -25,9 +29,9 @@ export const Switch = ({
|
|||
disabled = false,
|
||||
onChange: $onChange,
|
||||
field,
|
||||
form,
|
||||
form
|
||||
}: SwitchProps) => {
|
||||
const checked = field?.checked ?? $checked
|
||||
const checked = field?.checked ?? $checked;
|
||||
|
||||
return (
|
||||
<HeadlessSwitch.Group as="div" className="flex items-center space-x-4">
|
||||
|
@ -38,32 +42,32 @@ export const Switch = ({
|
|||
disabled={disabled}
|
||||
checked={checked}
|
||||
onChange={value => {
|
||||
form?.setFieldValue(field?.name ?? '', value)
|
||||
$onChange && $onChange(value)
|
||||
form?.setFieldValue(field?.name ?? "", value);
|
||||
$onChange && $onChange(value);
|
||||
}}
|
||||
|
||||
className={classNames(
|
||||
checked ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600',
|
||||
'ml-4 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'
|
||||
checked ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-600",
|
||||
"ml-4 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"
|
||||
)}
|
||||
>
|
||||
{({ checked }) => (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
checked ? 'translate-x-5' : 'translate-x-0',
|
||||
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
||||
checked ? "translate-x-5" : "translate-x-0",
|
||||
"inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</HeadlessSwitch>
|
||||
</HeadlessSwitch.Group>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export type SwitchFormikProps = SwitchProps & FieldProps & React.InputHTMLAttributes<HTMLInputElement>;
|
||||
|
||||
export const SwitchFormik = (props: SwitchProps) => <Switch {...props} />
|
||||
export const SwitchFormik = (props: SwitchProps) => <Switch {...props} children={props.children}/>;
|
||||
|
||||
interface SwitchGroupProps {
|
||||
name: string;
|
||||
|
@ -94,26 +98,26 @@ const SwitchGroup = ({
|
|||
<Field name={name} type="checkbox">
|
||||
{({
|
||||
field,
|
||||
form: { setFieldValue },
|
||||
}: any) => (
|
||||
form: { setFieldValue }
|
||||
}: FieldProps) => (
|
||||
<Switch
|
||||
{...field}
|
||||
type="button"
|
||||
// type="button"
|
||||
value={field.value}
|
||||
checked={field.checked}
|
||||
checked={field.checked ?? false}
|
||||
onChange={value => {
|
||||
setFieldValue(field?.name ?? '', value)
|
||||
setFieldValue(field?.name ?? "", value);
|
||||
}}
|
||||
className={classNames(
|
||||
field.value ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200',
|
||||
'ml-4 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'
|
||||
field.value ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200",
|
||||
"ml-4 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"
|
||||
)}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
field.value ? 'translate-x-5' : 'translate-x-0',
|
||||
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
||||
field.value ? "translate-x-5" : "translate-x-0",
|
||||
"inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
|
@ -123,4 +127,4 @@ const SwitchGroup = ({
|
|||
</HeadlessSwitch.Group>
|
||||
);
|
||||
|
||||
export { SwitchGroup }
|
||||
export { SwitchGroup };
|
|
@ -1,12 +1,12 @@
|
|||
import { Fragment, FC } from "react";
|
||||
import React, { Fragment, FC } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { ExclamationIcon } from "@heroicons/react/solid";
|
||||
|
||||
interface DeleteModalProps {
|
||||
isOpen: boolean;
|
||||
buttonRef: any;
|
||||
toggle: any;
|
||||
deleteAction: any;
|
||||
buttonRef: React.MutableRefObject<HTMLElement | null> | undefined;
|
||||
toggle: () => void;
|
||||
deleteAction: () => void;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ export const DeleteModal: FC<DeleteModalProps> = ({ isOpen, buttonRef, toggle, d
|
|||
type="button"
|
||||
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 dark:border-gray-600 shadow-sm px-4 py-2 bg-white dark:bg-gray-700 text-base font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
|
||||
onClick={toggle}
|
||||
ref={buttonRef}
|
||||
// ref={buttonRef}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
@ -89,4 +89,4 @@ export const DeleteModal: FC<DeleteModalProps> = ({ isOpen, buttonRef, toggle, d
|
|||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
)
|
||||
);
|
|
@ -1,30 +1,30 @@
|
|||
import { FC } from 'react'
|
||||
import { XIcon, CheckCircleIcon, ExclamationIcon, ExclamationCircleIcon } from '@heroicons/react/solid'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { classNames } from '../../utils'
|
||||
import { FC } from "react";
|
||||
import { XIcon, CheckCircleIcon, ExclamationIcon, ExclamationCircleIcon } from "@heroicons/react/solid";
|
||||
import { toast, Toast as Tooast } from "react-hot-toast";
|
||||
import { classNames } from "../../utils";
|
||||
|
||||
type Props = {
|
||||
type: 'error' | 'success' | 'warning'
|
||||
type: "error" | "success" | "warning"
|
||||
body?: string
|
||||
t?: any;
|
||||
}
|
||||
t?: Tooast;
|
||||
};
|
||||
|
||||
const Toast: FC<Props> = ({ type, body, t }) => (
|
||||
<div className={classNames(
|
||||
t.visible ? 'animate-enter' : 'animate-leave',
|
||||
t?.visible ? "animate-enter" : "animate-leave",
|
||||
"max-w-sm w-full bg-white dark:bg-gray-800 shadow-lg rounded-lg pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden transition-all")}>
|
||||
<div className="p-4">
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0">
|
||||
{type === 'success' && <CheckCircleIcon className="h-6 w-6 text-green-400" aria-hidden="true" />}
|
||||
{type === 'error' && <ExclamationCircleIcon className="h-6 w-6 text-red-400" aria-hidden="true" />}
|
||||
{type === 'warning' && <ExclamationIcon className="h-6 w-6 text-yellow-400" aria-hidden="true" />}
|
||||
{type === "success" && <CheckCircleIcon className="h-6 w-6 text-green-400" aria-hidden="true" />}
|
||||
{type === "error" && <ExclamationCircleIcon className="h-6 w-6 text-red-400" aria-hidden="true" />}
|
||||
{type === "warning" && <ExclamationIcon className="h-6 w-6 text-yellow-400" aria-hidden="true" />}
|
||||
</div>
|
||||
<div className="ml-3 w-0 flex-1 pt-0.5">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-gray-200">
|
||||
{type === 'success' && "Success"}
|
||||
{type === 'error' && "Error"}
|
||||
{type === 'warning' && "Warning"}
|
||||
{type === "success" && "Success"}
|
||||
{type === "error" && "Error"}
|
||||
{type === "warning" && "Warning"}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{body}</p>
|
||||
</div>
|
||||
|
@ -32,7 +32,7 @@ const Toast: FC<Props> = ({ type, body, t }) => (
|
|||
<button
|
||||
className="bg-white dark:bg-gray-700 rounded-md inline-flex text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||
onClick={() => {
|
||||
toast.dismiss(t.id)
|
||||
toast.dismiss(t?.id);
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Close</span>
|
||||
|
@ -42,6 +42,6 @@ const Toast: FC<Props> = ({ type, body, t }) => (
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
export default Toast;
|
|
@ -1,4 +1,4 @@
|
|||
import { Fragment, useRef } from "react";
|
||||
import React, { Fragment, useRef } from "react";
|
||||
import { XIcon } from "@heroicons/react/solid";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { Form, Formik } from "formik";
|
||||
|
@ -10,7 +10,7 @@ import { classNames } from "../../utils";
|
|||
interface SlideOverProps<DataType> {
|
||||
title: string;
|
||||
initialValues: DataType;
|
||||
validate?: (values?: any) => void;
|
||||
validate?: (values: DataType) => void;
|
||||
onSubmit: (values?: DataType) => void;
|
||||
isOpen: boolean;
|
||||
toggle: () => void;
|
||||
|
@ -30,7 +30,7 @@ function SlideOver<DataType>({
|
|||
type,
|
||||
children
|
||||
}: SlideOverProps<DataType>): React.ReactElement {
|
||||
const cancelModalButtonRef = useRef(null);
|
||||
const cancelModalButtonRef = useRef<HTMLInputElement | null>(null);
|
||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
||||
|
||||
return (
|
||||
|
@ -140,7 +140,7 @@ function SlideOver<DataType>({
|
|||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { SlideOver };
|
|
@ -1,3 +1,5 @@
|
|||
import { MultiSelectOption } from "../components/inputs/select";
|
||||
|
||||
export const resolutions = [
|
||||
"2160p",
|
||||
"1080p",
|
||||
|
@ -9,7 +11,7 @@ export const resolutions = [
|
|||
"480i"
|
||||
];
|
||||
|
||||
export const RESOLUTION_OPTIONS = resolutions.map(r => ({ value: r, label: r, key: r}));
|
||||
export const RESOLUTION_OPTIONS: MultiSelectOption[] = resolutions.map(r => ({ value: r, label: r, key: r }));
|
||||
|
||||
export const codecs = [
|
||||
"HEVC",
|
||||
|
@ -23,7 +25,7 @@ export const codecs = [
|
|||
"XviD"
|
||||
];
|
||||
|
||||
export const CODECS_OPTIONS = codecs.map(v => ({ value: v, label: v, key: v}));
|
||||
export const CODECS_OPTIONS: MultiSelectOption[] = codecs.map(v => ({ value: v, label: v, key: v }));
|
||||
|
||||
export const sources = [
|
||||
"BluRay",
|
||||
|
@ -46,18 +48,18 @@ export const sources = [
|
|||
"HDTS",
|
||||
"HDTV",
|
||||
"Mixed",
|
||||
"SiteRip",
|
||||
"SiteRip"
|
||||
];
|
||||
|
||||
export const SOURCES_OPTIONS = sources.map(v => ({ value: v, label: v, key: v}));
|
||||
export const SOURCES_OPTIONS: MultiSelectOption[] = sources.map(v => ({ value: v, label: v, key: v }));
|
||||
|
||||
export const containers = [
|
||||
"avi",
|
||||
"mp4",
|
||||
"mkv",
|
||||
"mkv"
|
||||
];
|
||||
|
||||
export const CONTAINER_OPTIONS = containers.map(v => ({ value: v, label: v, key: v}));
|
||||
export const CONTAINER_OPTIONS: MultiSelectOption[] = containers.map(v => ({ value: v, label: v, key: v }));
|
||||
|
||||
export const hdr = [
|
||||
"HDR",
|
||||
|
@ -69,18 +71,18 @@ export const hdr = [
|
|||
"DV HDR10",
|
||||
"DV HDR10+",
|
||||
"DoVi",
|
||||
"Dolby Vision",
|
||||
"Dolby Vision"
|
||||
];
|
||||
|
||||
export const HDR_OPTIONS = hdr.map(v => ({ value: v, label: v, key: v}));
|
||||
export const HDR_OPTIONS: MultiSelectOption[] = hdr.map(v => ({ value: v, label: v, key: v }));
|
||||
|
||||
export const quality_other = [
|
||||
"REMUX",
|
||||
"HYBRID",
|
||||
"REPACK",
|
||||
"REPACK"
|
||||
];
|
||||
|
||||
export const OTHER_OPTIONS = quality_other.map(v => ({ value: v, label: v, key: v}));
|
||||
export const OTHER_OPTIONS = quality_other.map(v => ({ value: v, label: v, key: v }));
|
||||
|
||||
export const formatMusic = [
|
||||
"MP3",
|
||||
|
@ -89,10 +91,10 @@ export const formatMusic = [
|
|||
"Ogg",
|
||||
"AAC",
|
||||
"AC3",
|
||||
"DTS",
|
||||
"DTS"
|
||||
];
|
||||
|
||||
export const FORMATS_OPTIONS = formatMusic.map(r => ({ value: r, label: r, key: r}));
|
||||
export const FORMATS_OPTIONS: MultiSelectOption[] = formatMusic.map(r => ({ value: r, label: r, key: r }));
|
||||
|
||||
export const sourcesMusic = [
|
||||
"CD",
|
||||
|
@ -103,10 +105,10 @@ export const sourcesMusic = [
|
|||
"DAT",
|
||||
"Cassette",
|
||||
"Blu-Ray",
|
||||
"SACD",
|
||||
"SACD"
|
||||
];
|
||||
|
||||
export const SOURCES_MUSIC_OPTIONS = sourcesMusic.map(v => ({ value: v, label: v, key: v}));
|
||||
export const SOURCES_MUSIC_OPTIONS: MultiSelectOption[] = sourcesMusic.map(v => ({ value: v, label: v, key: v }));
|
||||
|
||||
export const qualityMusic = [
|
||||
"192",
|
||||
|
@ -118,10 +120,10 @@ export const qualityMusic = [
|
|||
"V1 (VBR)",
|
||||
"V0 (VBR)",
|
||||
"Lossless",
|
||||
"24bit Lossless",
|
||||
"24bit Lossless"
|
||||
];
|
||||
|
||||
export const QUALITY_MUSIC_OPTIONS = qualityMusic.map(v => ({ value: v, label: v, key: v}));
|
||||
export const QUALITY_MUSIC_OPTIONS: MultiSelectOption[] = qualityMusic.map(v => ({ value: v, label: v, key: v }));
|
||||
|
||||
export const releaseTypeMusic = [
|
||||
"Album",
|
||||
|
@ -138,19 +140,19 @@ export const releaseTypeMusic = [
|
|||
"Demo",
|
||||
"Concert Recording",
|
||||
"DJ Mix",
|
||||
"Unknown",
|
||||
"Unknown"
|
||||
];
|
||||
|
||||
export const RELEASE_TYPE_MUSIC_OPTIONS = releaseTypeMusic.map(v => ({ value: v, label: v, key: v}));
|
||||
export const RELEASE_TYPE_MUSIC_OPTIONS: MultiSelectOption[] = releaseTypeMusic.map(v => ({ value: v, label: v, key: v }));
|
||||
|
||||
export const originOptions = [
|
||||
"P2P",
|
||||
"Internal",
|
||||
"SCENE",
|
||||
"O-SCENE",
|
||||
"O-SCENE"
|
||||
];
|
||||
|
||||
export const ORIGIN_OPTIONS = originOptions.map(v => ({ value: v, label: v, key: v}));
|
||||
export const ORIGIN_OPTIONS = originOptions.map(v => ({ value: v, label: v, key: v }));
|
||||
|
||||
export interface RadioFieldsetOption {
|
||||
label: string;
|
||||
|
@ -193,7 +195,7 @@ export const DownloadClientTypeOptions: RadioFieldsetOption[] = [
|
|||
label: "Whisparr",
|
||||
description: "Send to Whisparr and let it decide",
|
||||
value: "WHISPARR"
|
||||
},
|
||||
}
|
||||
];
|
||||
|
||||
export const DownloadClientTypeNameMap: Record<DownloadClientType | string, string> = {
|
||||
|
@ -203,21 +205,21 @@ export const DownloadClientTypeNameMap: Record<DownloadClientType | string, stri
|
|||
"RADARR": "Radarr",
|
||||
"SONARR": "Sonarr",
|
||||
"LIDARR": "Lidarr",
|
||||
"WHISPARR": "Whisparr",
|
||||
"WHISPARR": "Whisparr"
|
||||
};
|
||||
|
||||
export const ActionTypeOptions: RadioFieldsetOption[] = [
|
||||
{label: "Test", description: "A simple action to test a filter.", value: "TEST"},
|
||||
{label: "Watch dir", description: "Add filtered torrents to a watch directory", value: "WATCH_FOLDER"},
|
||||
{label: "Webhook", description: "Run webhook", value: "WEBHOOK"},
|
||||
{label: "Exec", description: "Run a custom command after a filter match", value: "EXEC"},
|
||||
{label: "qBittorrent", description: "Add torrents directly to qBittorrent", value: "QBITTORRENT"},
|
||||
{label: "Deluge", description: "Add torrents directly to Deluge", value: "DELUGE_V1"},
|
||||
{label: "Deluge v2", description: "Add torrents directly to Deluge 2", value: "DELUGE_V2"},
|
||||
{label: "Radarr", description: "Send to Radarr and let it decide", value: "RADARR"},
|
||||
{label: "Sonarr", description: "Send to Sonarr and let it decide", value: "SONARR"},
|
||||
{label: "Lidarr", description: "Send to Lidarr and let it decide", value: "LIDARR"},
|
||||
{label: "Whisparr", description: "Send to Whisparr and let it decide", value: "WHISPARR"},
|
||||
{ label: "Test", description: "A simple action to test a filter.", value: "TEST" },
|
||||
{ label: "Watch dir", description: "Add filtered torrents to a watch directory", value: "WATCH_FOLDER" },
|
||||
{ label: "Webhook", description: "Run webhook", value: "WEBHOOK" },
|
||||
{ label: "Exec", description: "Run a custom command after a filter match", value: "EXEC" },
|
||||
{ label: "qBittorrent", description: "Add torrents directly to qBittorrent", value: "QBITTORRENT" },
|
||||
{ label: "Deluge", description: "Add torrents directly to Deluge", value: "DELUGE_V1" },
|
||||
{ label: "Deluge v2", description: "Add torrents directly to Deluge 2", value: "DELUGE_V2" },
|
||||
{ label: "Radarr", description: "Send to Radarr and let it decide", value: "RADARR" },
|
||||
{ label: "Sonarr", description: "Send to Sonarr and let it decide", value: "SONARR" },
|
||||
{ label: "Lidarr", description: "Send to Lidarr and let it decide", value: "LIDARR" },
|
||||
{ label: "Whisparr", description: "Send to Whisparr and let it decide", value: "WHISPARR" }
|
||||
];
|
||||
|
||||
export const ActionTypeNameMap = {
|
||||
|
@ -231,13 +233,18 @@ export const ActionTypeNameMap = {
|
|||
"RADARR": "Radarr",
|
||||
"SONARR": "Sonarr",
|
||||
"LIDARR": "Lidarr",
|
||||
"WHISPARR": "Whisparr",
|
||||
"WHISPARR": "Whisparr"
|
||||
};
|
||||
|
||||
export const PushStatusOptions: any[] = [
|
||||
export interface OptionBasic {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const PushStatusOptions: OptionBasic[] = [
|
||||
{
|
||||
label: "Rejected",
|
||||
value: "PUSH_REJECTED",
|
||||
value: "PUSH_REJECTED"
|
||||
},
|
||||
{
|
||||
label: "Approved",
|
||||
|
@ -246,36 +253,36 @@ export const PushStatusOptions: any[] = [
|
|||
{
|
||||
label: "Error",
|
||||
value: "PUSH_ERROR"
|
||||
},
|
||||
}
|
||||
];
|
||||
|
||||
export const NotificationTypeOptions: any[] = [
|
||||
export const NotificationTypeOptions: OptionBasic[] = [
|
||||
{
|
||||
label: "Discord",
|
||||
value: "DISCORD",
|
||||
},
|
||||
value: "DISCORD"
|
||||
}
|
||||
];
|
||||
|
||||
export interface SelectOption {
|
||||
label: string;
|
||||
description: string;
|
||||
value: any;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const EventOptions: SelectOption[] = [
|
||||
{
|
||||
label: "Push Rejected",
|
||||
value: "PUSH_REJECTED",
|
||||
description: "On push rejected for the arrs or download client",
|
||||
description: "On push rejected for the arrs or download client"
|
||||
},
|
||||
{
|
||||
label: "Push Approved",
|
||||
value: "PUSH_APPROVED",
|
||||
description: "On push approved for the arrs or download client",
|
||||
description: "On push approved for the arrs or download client"
|
||||
},
|
||||
{
|
||||
label: "Push Error",
|
||||
value: "PUSH_ERROR",
|
||||
description: "On push error for the arrs or download client",
|
||||
},
|
||||
description: "On push error for the arrs or download client"
|
||||
}
|
||||
];
|
||||
|
|
4
web/src/domain/react-table-config.d.ts
vendored
4
web/src/domain/react-table-config.d.ts
vendored
|
@ -46,9 +46,9 @@ import {
|
|||
UseSortByInstanceProps,
|
||||
UseSortByOptions,
|
||||
UseSortByState
|
||||
} from 'react-table'
|
||||
} from "react-table";
|
||||
|
||||
declare module 'react-table' {
|
||||
declare module "react-table" {
|
||||
// take this file as-is, or comment out the sections that don't apply to your plugin configuration
|
||||
|
||||
export interface TableOptions<D extends Record<string, unknown>>
|
||||
|
|
|
@ -3,15 +3,20 @@ import { useMutation } from "react-query";
|
|||
import { toast } from "react-hot-toast";
|
||||
import { XIcon } from "@heroicons/react/solid";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { Field, Form, Formik } from "formik";
|
||||
import { Field, Form, Formik, FormikErrors, FormikValues } from "formik";
|
||||
import type { FieldProps } from "formik";
|
||||
|
||||
import { queryClient } from "../../App";
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
import DEBUG from "../../components/debug";
|
||||
import Toast from '../../components/notifications/Toast';
|
||||
import Toast from "../../components/notifications/Toast";
|
||||
|
||||
function FilterAddForm({ isOpen, toggle }: any) {
|
||||
interface filterAddFormProps {
|
||||
isOpen: boolean;
|
||||
toggle: () => void;
|
||||
}
|
||||
|
||||
function FilterAddForm({ isOpen, toggle }: filterAddFormProps) {
|
||||
const mutation = useMutation(
|
||||
(filter: Filter) => APIClient.filters.create(filter),
|
||||
{
|
||||
|
@ -22,10 +27,16 @@ function FilterAddForm({ isOpen, toggle }: any) {
|
|||
toggle();
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const handleSubmit = (data: any) => mutation.mutate(data);
|
||||
const validate = (values: any) => values.name ? {} : { name: "Required" };
|
||||
const handleSubmit = (data: unknown) => mutation.mutate(data as Filter);
|
||||
const validate = (values: FormikValues) => {
|
||||
const errors = {} as FormikErrors<FormikValues>;
|
||||
if (!values.name) {
|
||||
errors.name = "Required";
|
||||
}
|
||||
return errors;
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
|
@ -53,7 +64,7 @@ function FilterAddForm({ isOpen, toggle }: any) {
|
|||
codecs: [],
|
||||
sources: [],
|
||||
containers: [],
|
||||
origins: [],
|
||||
origins: []
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
validate={validate}
|
||||
|
@ -97,7 +108,7 @@ function FilterAddForm({ isOpen, toggle }: any) {
|
|||
<Field name="name">
|
||||
{({
|
||||
field,
|
||||
meta,
|
||||
meta
|
||||
}: FieldProps ) => (
|
||||
<div className="sm:col-span-2">
|
||||
<input
|
||||
|
@ -145,7 +156,7 @@ function FilterAddForm({ isOpen, toggle }: any) {
|
|||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default FilterAddForm;
|
|
@ -1,20 +1,21 @@
|
|||
import { Fragment, useRef, useState } from "react";
|
||||
import React, { Fragment, useRef, useState } from "react";
|
||||
import { useMutation } from "react-query";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { XIcon } from "@heroicons/react/solid";
|
||||
import { sleep, classNames } from "../../utils";
|
||||
import { classNames, sleep } from "../../utils";
|
||||
import { Form, Formik, useFormikContext } from "formik";
|
||||
import DEBUG from "../../components/debug";
|
||||
import { queryClient } from "../../App";
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
import { DownloadClientTypeOptions } from "../../domain/constants";
|
||||
|
||||
import { toast } from 'react-hot-toast'
|
||||
import Toast from '../../components/notifications/Toast';
|
||||
import { toast } from "react-hot-toast";
|
||||
import Toast from "../../components/notifications/Toast";
|
||||
import { useToggle } from "../../hooks/hooks";
|
||||
import { DeleteModal } from "../../components/modals";
|
||||
import { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "../../components/inputs/input_wide";
|
||||
import { RadioFieldsetWide } from "../../components/inputs/radio";
|
||||
import DownloadClient from "../../screens/settings/DownloadClient";
|
||||
|
||||
interface InitialValuesSettings {
|
||||
basic?: {
|
||||
|
@ -46,70 +47,73 @@ interface InitialValues {
|
|||
|
||||
function FormFieldsDefault() {
|
||||
const {
|
||||
values: { tls },
|
||||
values: { tls }
|
||||
} = useFormikContext<InitialValues>();
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<TextFieldWide name="host" label="Host" help="Eg. client.domain.ltd, domain.ltd/client, domain.ltd:port" />
|
||||
<TextFieldWide name="host" label="Host" help="Eg. client.domain.ltd, domain.ltd/client, domain.ltd:port"/>
|
||||
|
||||
<NumberFieldWide name="port" label="Port" help="WebUI port for qBittorrent and daemon port for Deluge" />
|
||||
<NumberFieldWide name="port" label="Port" help="WebUI port for qBittorrent and daemon port for Deluge"/>
|
||||
|
||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:divide-gray-700">
|
||||
<SwitchGroupWide name="tls" label="TLS" />
|
||||
<SwitchGroupWide name="tls" label="TLS"/>
|
||||
|
||||
{tls && (
|
||||
<Fragment>
|
||||
<SwitchGroupWide name="tls_skip_verify" label="Skip TLS verification (insecure)" />
|
||||
<SwitchGroupWide name="tls_skip_verify" label="Skip TLS verification (insecure)"/>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<TextFieldWide name="username" label="Username" />
|
||||
<PasswordFieldWide name="password" label="Password" />
|
||||
<TextFieldWide name="username" label="Username"/>
|
||||
<PasswordFieldWide name="password" label="Password"/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
function FormFieldsArr() {
|
||||
const {
|
||||
values: { settings },
|
||||
values: { settings }
|
||||
} = useFormikContext<InitialValues>();
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<TextFieldWide name="host" label="Host" help="Full url http(s)://domain.ltd and/or subdomain/subfolder" />
|
||||
<TextFieldWide name="host" label="Host" help="Full url http(s)://domain.ltd and/or subdomain/subfolder"/>
|
||||
|
||||
<PasswordFieldWide name="settings.apikey" label="API key" />
|
||||
<PasswordFieldWide name="settings.apikey" label="API key"/>
|
||||
|
||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||
<SwitchGroupWide name="settings.basic.auth" label="Basic auth" />
|
||||
<SwitchGroupWide name="settings.basic.auth" label="Basic auth"/>
|
||||
</div>
|
||||
|
||||
{settings.basic?.auth === true && (
|
||||
<Fragment>
|
||||
<TextFieldWide name="settings.basic.username" label="Username" />
|
||||
<PasswordFieldWide name="settings.basic.password" label="Password" />
|
||||
<TextFieldWide name="settings.basic.username" label="Username"/>
|
||||
<PasswordFieldWide name="settings.basic.password" label="Password"/>
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export const componentMap: any = {
|
||||
DELUGE_V1: <FormFieldsDefault />,
|
||||
DELUGE_V2: <FormFieldsDefault />,
|
||||
QBITTORRENT: <FormFieldsDefault />,
|
||||
RADARR: <FormFieldsArr />,
|
||||
SONARR: <FormFieldsArr />,
|
||||
LIDARR: <FormFieldsArr />,
|
||||
WHISPARR: <FormFieldsArr />,
|
||||
};
|
||||
export interface componentMapType {
|
||||
[key: string]: React.ReactElement;
|
||||
}
|
||||
|
||||
export const componentMap: componentMapType = {
|
||||
DELUGE_V1: <FormFieldsDefault/>,
|
||||
DELUGE_V2: <FormFieldsDefault/>,
|
||||
QBITTORRENT: <FormFieldsDefault/>,
|
||||
RADARR: <FormFieldsArr/>,
|
||||
SONARR: <FormFieldsArr/>,
|
||||
LIDARR: <FormFieldsArr/>,
|
||||
WHISPARR: <FormFieldsArr/>
|
||||
};
|
||||
|
||||
function FormFieldsRulesBasic() {
|
||||
const {
|
||||
values: { settings },
|
||||
values: { settings }
|
||||
} = useFormikContext<InitialValues>();
|
||||
|
||||
return (
|
||||
|
@ -123,12 +127,12 @@ function FormFieldsRulesBasic() {
|
|||
</div>
|
||||
|
||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||
<SwitchGroupWide name="settings.rules.enabled" label="Enabled" />
|
||||
<SwitchGroupWide name="settings.rules.enabled" label="Enabled"/>
|
||||
</div>
|
||||
|
||||
{settings && settings.rules?.enabled === true && (
|
||||
<Fragment>
|
||||
<NumberFieldWide name="settings.rules.max_active_downloads" label="Max active downloads" />
|
||||
<NumberFieldWide name="settings.rules.max_active_downloads" label="Max active downloads"/>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
|
@ -137,7 +141,7 @@ function FormFieldsRulesBasic() {
|
|||
|
||||
function FormFieldsRules() {
|
||||
const {
|
||||
values: { settings },
|
||||
values: { settings }
|
||||
} = useFormikContext<InitialValues>();
|
||||
|
||||
return (
|
||||
|
@ -151,19 +155,21 @@ function FormFieldsRules() {
|
|||
</div>
|
||||
|
||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||
<SwitchGroupWide name="settings.rules.enabled" label="Enabled" />
|
||||
<SwitchGroupWide name="settings.rules.enabled" label="Enabled"/>
|
||||
</div>
|
||||
|
||||
{settings.rules?.enabled === true && (
|
||||
<Fragment>
|
||||
<NumberFieldWide name="settings.rules.max_active_downloads" label="Max active downloads" />
|
||||
<NumberFieldWide name="settings.rules.max_active_downloads" label="Max active downloads"/>
|
||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||
<SwitchGroupWide name="settings.rules.ignore_slow_torrents" label="Ignore slow torrents" />
|
||||
<SwitchGroupWide name="settings.rules.ignore_slow_torrents" label="Ignore slow torrents"/>
|
||||
</div>
|
||||
|
||||
{settings.rules?.ignore_slow_torrents === true && (
|
||||
<Fragment>
|
||||
<NumberFieldWide name="settings.rules.download_speed_threshold" label="Download speed threshold" placeholder="in KB/s" help="If download speed is below this when max active downloads is hit, download anyways. KB/s" />
|
||||
<NumberFieldWide name="settings.rules.download_speed_threshold" label="Download speed threshold"
|
||||
placeholder="in KB/s"
|
||||
help="If download speed is below this when max active downloads is hit, download anyways. KB/s"/>
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
|
@ -172,28 +178,37 @@ function FormFieldsRules() {
|
|||
);
|
||||
}
|
||||
|
||||
export const rulesComponentMap: any = {
|
||||
DELUGE_V1: <FormFieldsRulesBasic />,
|
||||
DELUGE_V2: <FormFieldsRulesBasic />,
|
||||
QBITTORRENT: <FormFieldsRules />,
|
||||
export const rulesComponentMap: componentMapType = {
|
||||
DELUGE_V1: <FormFieldsRulesBasic/>,
|
||||
DELUGE_V2: <FormFieldsRulesBasic/>,
|
||||
QBITTORRENT: <FormFieldsRules/>
|
||||
};
|
||||
|
||||
interface formButtonsProps {
|
||||
isSuccessfulTest: boolean;
|
||||
isErrorTest: boolean;
|
||||
isTesting: boolean;
|
||||
cancelFn: any;
|
||||
testFn: any;
|
||||
values: any;
|
||||
cancelFn: () => void;
|
||||
testFn: (data: unknown) => void;
|
||||
values: unknown;
|
||||
type: "CREATE" | "UPDATE";
|
||||
toggleDeleteModal?: any;
|
||||
toggleDeleteModal?: () => void;
|
||||
}
|
||||
|
||||
function DownloadClientFormButtons({ type, isSuccessfulTest, isErrorTest, isTesting, cancelFn, testFn, values, toggleDeleteModal }: formButtonsProps) {
|
||||
function DownloadClientFormButtons({
|
||||
type,
|
||||
isSuccessfulTest,
|
||||
isErrorTest,
|
||||
isTesting,
|
||||
cancelFn,
|
||||
testFn,
|
||||
values,
|
||||
toggleDeleteModal
|
||||
}: formButtonsProps) {
|
||||
|
||||
const test = () => {
|
||||
testFn(values)
|
||||
}
|
||||
testFn(values);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
|
||||
|
@ -269,10 +284,15 @@ function DownloadClientFormButtons({ type, isSuccessfulTest, isErrorTest, isTest
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function DownloadClientAddForm({ isOpen, toggle }: any) {
|
||||
interface formProps {
|
||||
isOpen: boolean;
|
||||
toggle: () => void;
|
||||
}
|
||||
|
||||
export function DownloadClientAddForm({ isOpen, toggle }: formProps) {
|
||||
const [isTesting, setIsTesting] = useState(false);
|
||||
const [isSuccessfulTest, setIsSuccessfulTest] = useState(false);
|
||||
const [isErrorTest, setIsErrorTest] = useState(false);
|
||||
|
@ -282,12 +302,12 @@ export function DownloadClientAddForm({ isOpen, toggle }: any) {
|
|||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["downloadClients"]);
|
||||
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();
|
||||
},
|
||||
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}/>);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -313,22 +333,22 @@ export function DownloadClientAddForm({ isOpen, toggle }: any) {
|
|||
});
|
||||
},
|
||||
onError: () => {
|
||||
console.log('not added')
|
||||
console.log("not added");
|
||||
setIsTesting(false);
|
||||
setIsErrorTest(true);
|
||||
sleep(2500).then(() => {
|
||||
setIsErrorTest(false);
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const onSubmit = (data: any) => {
|
||||
mutation.mutate(data);
|
||||
const onSubmit = (data: unknown) => {
|
||||
mutation.mutate(data as DownloadClient);
|
||||
};
|
||||
|
||||
const testClient = (data: any) => {
|
||||
testClientMutation.mutate(data);
|
||||
const testClient = (data: unknown) => {
|
||||
testClientMutation.mutate(data as DownloadClient);
|
||||
};
|
||||
|
||||
const initialValues: InitialValues = {
|
||||
|
@ -342,7 +362,7 @@ export function DownloadClientAddForm({ isOpen, toggle }: any) {
|
|||
username: "",
|
||||
password: "",
|
||||
settings: {}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
|
@ -354,7 +374,7 @@ export function DownloadClientAddForm({ isOpen, toggle }: any) {
|
|||
onClose={toggle}
|
||||
>
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
<Dialog.Overlay className="absolute inset-0" />
|
||||
<Dialog.Overlay className="absolute inset-0"/>
|
||||
|
||||
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
|
||||
<Transition.Child
|
||||
|
@ -404,10 +424,11 @@ export function DownloadClientAddForm({ isOpen, toggle }: any) {
|
|||
</div>
|
||||
|
||||
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700">
|
||||
<TextFieldWide name="name" label="Name" />
|
||||
<TextFieldWide name="name" label="Name"/>
|
||||
|
||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:divide-gray-700">
|
||||
<SwitchGroupWide name="enabled" label="Enabled" />
|
||||
<div
|
||||
className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:divide-gray-700">
|
||||
<SwitchGroupWide name="enabled" label="Enabled"/>
|
||||
</div>
|
||||
|
||||
<RadioFieldsetWide
|
||||
|
@ -432,7 +453,7 @@ export function DownloadClientAddForm({ isOpen, toggle }: any) {
|
|||
values={values}
|
||||
/>
|
||||
|
||||
<DEBUG values={values} />
|
||||
<DEBUG values={values}/>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
|
@ -445,7 +466,13 @@ export function DownloadClientAddForm({ isOpen, toggle }: any) {
|
|||
);
|
||||
}
|
||||
|
||||
export function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
|
||||
interface updateFormProps {
|
||||
isOpen: boolean;
|
||||
toggle: () => void;
|
||||
client: DownloadClient;
|
||||
}
|
||||
|
||||
export function DownloadClientUpdateForm({ client, isOpen, toggle }: updateFormProps) {
|
||||
const [isTesting, setIsTesting] = useState(false);
|
||||
const [isSuccessfulTest, setIsSuccessfulTest] = useState(false);
|
||||
const [isErrorTest, setIsErrorTest] = useState(false);
|
||||
|
@ -456,9 +483,9 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
|
|||
{
|
||||
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}/>);
|
||||
toggle();
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -467,9 +494,9 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
|
|||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries();
|
||||
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();
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -499,12 +526,12 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
|
|||
sleep(2500).then(() => {
|
||||
setIsErrorTest(false);
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const onSubmit = (data: any) => {
|
||||
mutation.mutate(data);
|
||||
const onSubmit = (data: unknown) => {
|
||||
mutation.mutate(data as DownloadClient);
|
||||
};
|
||||
|
||||
const cancelButtonRef = useRef(null);
|
||||
|
@ -514,8 +541,8 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
|
|||
deleteMutation.mutate(client.id);
|
||||
};
|
||||
|
||||
const testClient = (data: any) => {
|
||||
testClientMutation.mutate(data);
|
||||
const testClient = (data: unknown) => {
|
||||
testClientMutation.mutate(data as DownloadClient);
|
||||
};
|
||||
|
||||
const initialValues = {
|
||||
|
@ -529,8 +556,8 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
|
|||
tls_skip_verify: client.tls_skip_verify,
|
||||
username: client.username,
|
||||
password: client.password,
|
||||
settings: client.settings,
|
||||
}
|
||||
settings: client.settings
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
|
@ -551,7 +578,7 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
|
|||
text="Are you sure you want to remove this download client? This action cannot be undone."
|
||||
/>
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
<Dialog.Overlay className="absolute inset-0" />
|
||||
<Dialog.Overlay className="absolute inset-0"/>
|
||||
|
||||
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
|
||||
<Transition.Child
|
||||
|
@ -602,10 +629,10 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
|
|||
</div>
|
||||
|
||||
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700">
|
||||
<TextFieldWide name="name" label="Name" />
|
||||
<TextFieldWide name="name" label="Name"/>
|
||||
|
||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||
<SwitchGroupWide name="enabled" label="Enabled" />
|
||||
<SwitchGroupWide name="enabled" label="Enabled"/>
|
||||
</div>
|
||||
|
||||
<RadioFieldsetWide
|
||||
|
@ -631,7 +658,7 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
|
|||
values={values}
|
||||
/>
|
||||
|
||||
<DEBUG values={values} />
|
||||
<DEBUG values={values}/>
|
||||
</Form>
|
||||
);
|
||||
}}
|
||||
|
|
|
@ -1,27 +1,28 @@
|
|||
import {useMutation} from "react-query";
|
||||
import {APIClient} from "../../api/APIClient";
|
||||
import {queryClient} from "../../App";
|
||||
import {toast} from "react-hot-toast";
|
||||
import { useMutation } from "react-query";
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
import { queryClient } from "../../App";
|
||||
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 {ImplementationMap} from "../../screens/settings/Feed";
|
||||
import { SlideOver } from "../../components/panels";
|
||||
import { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "../../components/inputs";
|
||||
import { ImplementationMap } from "../../screens/settings/Feed";
|
||||
import { componentMapType } from "./DownloadClientForms";
|
||||
|
||||
interface UpdateProps {
|
||||
isOpen: boolean;
|
||||
toggle: any;
|
||||
toggle: () => void;
|
||||
feed: Feed;
|
||||
}
|
||||
|
||||
export function FeedUpdateForm({isOpen, toggle, feed}: UpdateProps) {
|
||||
export function FeedUpdateForm({ isOpen, toggle, feed }: UpdateProps) {
|
||||
const mutation = useMutation(
|
||||
(feed: Feed) => APIClient.feeds.update(feed),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["feeds"]);
|
||||
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();
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -30,14 +31,14 @@ export function FeedUpdateForm({isOpen, toggle, feed}: UpdateProps) {
|
|||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["feeds"]);
|
||||
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 onSubmit = (formData: any) => {
|
||||
mutation.mutate(formData);
|
||||
}
|
||||
const onSubmit = (formData: unknown) => {
|
||||
mutation.mutate(formData as Feed);
|
||||
};
|
||||
|
||||
const deleteAction = () => {
|
||||
deleteMutation.mutate(feed.id);
|
||||
|
@ -51,8 +52,8 @@ export function FeedUpdateForm({isOpen, toggle, feed}: UpdateProps) {
|
|||
name: feed.name,
|
||||
url: feed.url,
|
||||
api_key: feed.api_key,
|
||||
interval: feed.interval,
|
||||
}
|
||||
interval: feed.interval
|
||||
};
|
||||
|
||||
return (
|
||||
<SlideOver
|
||||
|
@ -69,7 +70,8 @@ export function FeedUpdateForm({isOpen, toggle, feed}: UpdateProps) {
|
|||
<TextFieldWide name="name" label="Name" required={true}/>
|
||||
|
||||
<div className="space-y-4 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="py-4 flex items-center justify-between space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
||||
<div
|
||||
className="py-4 flex items-center justify-between space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="type"
|
||||
|
@ -91,7 +93,7 @@ export function FeedUpdateForm({isOpen, toggle, feed}: UpdateProps) {
|
|||
</div>
|
||||
)}
|
||||
</SlideOver>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function FormFieldsTorznab() {
|
||||
|
@ -103,13 +105,14 @@ function FormFieldsTorznab() {
|
|||
help="Torznab url"
|
||||
/>
|
||||
|
||||
<PasswordFieldWide name="api_key" label="API key" />
|
||||
<PasswordFieldWide name="api_key" label="API key"/>
|
||||
|
||||
<NumberFieldWide name="interval" label="Refresh interval" help="Minutes. Recommended 15-30. To low and risk ban." />
|
||||
<NumberFieldWide name="interval" label="Refresh interval"
|
||||
help="Minutes. Recommended 15-30. To low and risk ban."/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const componentMap: any = {
|
||||
TORZNAB: <FormFieldsTorznab/>,
|
||||
const componentMap: componentMapType = {
|
||||
TORZNAB: <FormFieldsTorznab/>
|
||||
};
|
|
@ -1,14 +1,20 @@
|
|||
import {Fragment, useState} from "react";
|
||||
import { Fragment, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useMutation, useQuery } from "react-query";
|
||||
import Select, { components } from "react-select";
|
||||
import { Field, Form, Formik } from "formik";
|
||||
import Select, {
|
||||
components,
|
||||
ControlProps,
|
||||
InputProps,
|
||||
MenuProps,
|
||||
OptionProps
|
||||
} from "react-select";
|
||||
import { Field, Form, Formik, FormikValues } from "formik";
|
||||
import type { FieldProps } from "formik";
|
||||
|
||||
import { XIcon } from "@heroicons/react/solid";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
||||
import {sleep, slugify} from "../../utils";
|
||||
import { sleep, slugify } from "../../utils";
|
||||
import { queryClient } from "../../App";
|
||||
import DEBUG from "../../components/debug";
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
|
@ -18,44 +24,48 @@ import {
|
|||
SwitchGroupWide
|
||||
} from "../../components/inputs";
|
||||
import { SlideOver } from "../../components/panels";
|
||||
import Toast from '../../components/notifications/Toast';
|
||||
import Toast from "../../components/notifications/Toast";
|
||||
|
||||
const Input = (props: any) => {
|
||||
const Input = (props: InputProps) => {
|
||||
return (
|
||||
<components.Input
|
||||
{...props}
|
||||
inputClassName="outline-none border-none shadow-none focus:ring-transparent"
|
||||
className="text-gray-400 dark:text-gray-100"
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const Control = (props: any) => {
|
||||
const Control = (props: ControlProps) => {
|
||||
return (
|
||||
<components.Control
|
||||
{...props}
|
||||
className="block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const Menu = (props: any) => {
|
||||
const Menu = (props: MenuProps) => {
|
||||
return (
|
||||
<components.Menu
|
||||
{...props}
|
||||
className="dark:bg-gray-800 border border-gray-300 dark:border-gray-700 dark:text-gray-400 rounded-md shadow-sm"
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const Option = (props: any) => {
|
||||
const Option = (props: OptionProps) => {
|
||||
return (
|
||||
<components.Option
|
||||
{...props}
|
||||
className="dark:text-gray-400 dark:bg-gray-800 dark:hover:bg-gray-900 dark:focus:bg-gray-900"
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const IrcSettingFields = (ind: IndexerDefinition, indexer: string) => {
|
||||
if (indexer !== "") {
|
||||
|
@ -72,21 +82,21 @@ const IrcSettingFields = (ind: IndexerDefinition, indexer: string) => {
|
|||
{ind.irc.settings.map((f: IndexerSetting, idx: number) => {
|
||||
switch (f.type) {
|
||||
case "text":
|
||||
return <TextFieldWide name={`irc.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} />
|
||||
return <TextFieldWide name={`irc.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} />;
|
||||
case "secret":
|
||||
if (f.name === "invite_command") {
|
||||
return <PasswordFieldWide name={`irc.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultVisible={true} defaultValue={f.default} />
|
||||
return <PasswordFieldWide name={`irc.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultVisible={true} defaultValue={f.default} />;
|
||||
}
|
||||
return <PasswordFieldWide name={`irc.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultValue={f.default} />
|
||||
return <PasswordFieldWide name={`irc.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultValue={f.default} />;
|
||||
}
|
||||
return null
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const FeedSettingFields = (ind: IndexerDefinition, indexer: string) => {
|
||||
if (indexer !== "") {
|
||||
|
@ -106,48 +116,48 @@ const FeedSettingFields = (ind: IndexerDefinition, indexer: string) => {
|
|||
{ind.torznab.settings.map((f: IndexerSetting, idx: number) => {
|
||||
switch (f.type) {
|
||||
case "text":
|
||||
return <TextFieldWide name={`feed.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} />
|
||||
return <TextFieldWide name={`feed.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} />;
|
||||
case "secret":
|
||||
return <PasswordFieldWide name={`feed.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultValue={f.default} />
|
||||
return <PasswordFieldWide name={`feed.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultValue={f.default} />;
|
||||
}
|
||||
return null
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const SettingFields = (ind: IndexerDefinition, indexer: string) => {
|
||||
if (indexer !== "") {
|
||||
return (
|
||||
<div key="opt">
|
||||
{ind && ind.settings && ind.settings.map((f: any, idx: number) => {
|
||||
{ind && ind.settings && ind.settings.map((f, idx: number) => {
|
||||
switch (f.type) {
|
||||
case "text":
|
||||
return (
|
||||
<TextFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} defaultValue="" />
|
||||
)
|
||||
);
|
||||
case "secret":
|
||||
return (
|
||||
<PasswordFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} defaultValue="" />
|
||||
)
|
||||
);
|
||||
}
|
||||
return null
|
||||
return null;
|
||||
})}
|
||||
<div hidden={true}>
|
||||
<TextFieldWide name="name" label="Name" defaultValue={ind?.name} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function slugIdentifier(name: string) {
|
||||
const l = name.toLowerCase()
|
||||
const r = l.replaceAll("torznab", "")
|
||||
return slugify(`torznab-${r}`)
|
||||
const l = name.toLowerCase();
|
||||
const r = l.replaceAll("torznab", "");
|
||||
return slugify(`torznab-${r}`);
|
||||
}
|
||||
|
||||
// interface initialValues {
|
||||
|
@ -160,33 +170,38 @@ function slugIdentifier(name: string) {
|
|||
// settings?: Record<string, unknown>;
|
||||
// }
|
||||
|
||||
type SelectValue = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
interface AddProps {
|
||||
isOpen: boolean;
|
||||
toggle: any;
|
||||
toggle: () => void;
|
||||
}
|
||||
|
||||
export function IndexerAddForm({ isOpen, toggle }: AddProps) {
|
||||
const [indexer, setIndexer] = useState<IndexerDefinition>({} as IndexerDefinition)
|
||||
const [indexer, setIndexer] = useState<IndexerDefinition>({} as IndexerDefinition);
|
||||
|
||||
const { data } = useQuery('indexerDefinition', APIClient.indexers.getSchema,
|
||||
const { data } = useQuery("indexerDefinition", APIClient.indexers.getSchema,
|
||||
{
|
||||
enabled: isOpen,
|
||||
refetchOnWindowFocus: false
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const mutation = useMutation(
|
||||
(indexer: Indexer) => APIClient.indexers.create(indexer), {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['indexer']);
|
||||
toast.custom((t) => <Toast type="success" body="Indexer was added" t={t} />)
|
||||
sleep(1500)
|
||||
toggle()
|
||||
queryClient.invalidateQueries(["indexer"]);
|
||||
toast.custom((t) => <Toast type="success" body="Indexer was added" t={t} />);
|
||||
sleep(1500);
|
||||
toggle();
|
||||
},
|
||||
onError: () => {
|
||||
toast.custom((t) => <Toast type="error" body="Indexer could not be added" t={t} />)
|
||||
toast.custom((t) => <Toast type="error" body="Indexer could not be added" t={t} />);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const ircMutation = useMutation(
|
||||
(network: IrcNetworkCreate) => APIClient.irc.createNetwork(network)
|
||||
|
@ -196,14 +211,14 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
|
|||
(feed: FeedCreate) => APIClient.feeds.create(feed)
|
||||
);
|
||||
|
||||
const onSubmit = (formData: any) => {
|
||||
const onSubmit = (formData: FormikValues) => {
|
||||
const ind = data && data.find(i => i.identifier === formData.identifier);
|
||||
if (!ind)
|
||||
return;
|
||||
|
||||
if (formData.implementation === "torznab") {
|
||||
// create slug for indexer identifier as "torznab-indexer_name"
|
||||
const name = slugIdentifier(formData.name)
|
||||
const name = slugIdentifier(formData.name);
|
||||
|
||||
const createFeed: FeedCreate = {
|
||||
name: formData.name,
|
||||
|
@ -213,14 +228,15 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
|
|||
api_key: formData.feed.api_key,
|
||||
interval: 30,
|
||||
indexer: name,
|
||||
indexer_id: 0,
|
||||
}
|
||||
indexer_id: 0
|
||||
};
|
||||
|
||||
mutation.mutate(formData, {
|
||||
mutation.mutate(formData as Indexer, {
|
||||
onSuccess: (indexer) => {
|
||||
createFeed.indexer_id = indexer!.id
|
||||
// @eslint-ignore
|
||||
createFeed.indexer_id = indexer.id;
|
||||
|
||||
feedMutation.mutate(createFeed)
|
||||
feedMutation.mutate(createFeed);
|
||||
}
|
||||
});
|
||||
return;
|
||||
|
@ -252,12 +268,12 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
|
|||
tls: ind.irc.tls,
|
||||
nickserv: formData.irc.nickserv,
|
||||
invite_command: formData.irc.invite_command,
|
||||
channels: channels,
|
||||
}
|
||||
channels: channels
|
||||
};
|
||||
|
||||
mutation.mutate(formData, {
|
||||
mutation.mutate(formData as Indexer, {
|
||||
onSuccess: () => {
|
||||
ircMutation.mutate(network)
|
||||
ircMutation.mutate(network);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -287,9 +303,10 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
|
|||
identifier: "",
|
||||
implementation: "irc",
|
||||
name: "",
|
||||
irc: {},
|
||||
feed: {},
|
||||
settings: {},
|
||||
irc: {
|
||||
invite_command: ""
|
||||
},
|
||||
settings: {}
|
||||
}}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
|
@ -348,25 +365,27 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
|
|||
spacing: {
|
||||
...theme.spacing,
|
||||
controlHeight: 30,
|
||||
baseUnit: 2,
|
||||
baseUnit: 2
|
||||
}
|
||||
})}
|
||||
value={field?.value && field.value.value}
|
||||
onChange={(option: any) => {
|
||||
resetForm()
|
||||
setFieldValue("name", option?.label ?? "")
|
||||
setFieldValue(field.name, option?.value ?? "")
|
||||
onChange={(option: unknown) => {
|
||||
const opt = option as SelectValue;
|
||||
resetForm();
|
||||
setFieldValue("name", opt.label ?? "");
|
||||
setFieldValue(field.name, opt.value ?? "");
|
||||
|
||||
const ind = data!.find(i => i.identifier === option.value);
|
||||
setFieldValue("implementation", ind?.implementation ? ind.implementation : "irc")
|
||||
setIndexer(ind!)
|
||||
if (ind!.irc?.settings) {
|
||||
ind!.irc.settings.forEach((s) => {
|
||||
setFieldValue(`irc.${s.name}`, s.default ?? "")
|
||||
})
|
||||
const ind = data && data.find(i => i.identifier === opt.value);
|
||||
if (ind) {
|
||||
setIndexer(ind);
|
||||
if (ind.irc.settings) {
|
||||
ind.irc.settings.forEach((s) => {
|
||||
setFieldValue(`irc.${s.name}`, s.default ?? "");
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
options={data && data.sort((a, b): any => a.name.localeCompare(b.name)).map(v => ({
|
||||
options={data && data.sort((a, b) => a.name.localeCompare(b.name)).map(v => ({
|
||||
label: v.name,
|
||||
value: v.identifier
|
||||
}))}
|
||||
|
@ -419,48 +438,45 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
|
|||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
interface UpdateProps {
|
||||
isOpen: boolean;
|
||||
toggle: any;
|
||||
indexer: Indexer;
|
||||
toggle: () => void;
|
||||
indexer: IndexerDefinition;
|
||||
}
|
||||
|
||||
export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) {
|
||||
const mutation = useMutation((indexer: Indexer) => APIClient.indexers.update(indexer), {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['indexer']);
|
||||
toast.custom((t) => <Toast type="success" body={`${indexer.name} was updated successfully`} t={t} />)
|
||||
sleep(1500)
|
||||
|
||||
toggle()
|
||||
}
|
||||
})
|
||||
|
||||
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} />)
|
||||
queryClient.invalidateQueries(["indexer"]);
|
||||
toast.custom((t) => <Toast type="success" body={`${indexer.name} was updated successfully`} t={t} />);
|
||||
sleep(1500);
|
||||
|
||||
toggle();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const onSubmit = (data: any) => {
|
||||
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} />);
|
||||
}
|
||||
});
|
||||
|
||||
const onSubmit = (data: unknown) => {
|
||||
// TODO clear data depending on type
|
||||
mutation.mutate(data)
|
||||
mutation.mutate(data as Indexer);
|
||||
};
|
||||
|
||||
const deleteAction = () => {
|
||||
deleteMutation.mutate(indexer.id)
|
||||
}
|
||||
deleteMutation.mutate(indexer.id ?? 0);
|
||||
};
|
||||
|
||||
const renderSettingFields = (settings: IndexerSetting[]) => {
|
||||
if (settings === undefined || settings === null) {
|
||||
return null
|
||||
if (settings === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -470,17 +486,17 @@ export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) {
|
|||
case "text":
|
||||
return (
|
||||
<TextFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} />
|
||||
)
|
||||
);
|
||||
case "secret":
|
||||
return (
|
||||
<PasswordFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} />
|
||||
)
|
||||
);
|
||||
}
|
||||
return null
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const initialValues = {
|
||||
id: indexer.id,
|
||||
|
@ -494,8 +510,8 @@ export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) {
|
|||
[obj.name]: obj.value
|
||||
} as Record<string, string>),
|
||||
{} as Record<string, string>
|
||||
),
|
||||
}
|
||||
)
|
||||
};
|
||||
|
||||
return (
|
||||
<SlideOver
|
||||
|
@ -540,5 +556,5 @@ export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) {
|
|||
</div>
|
||||
)}
|
||||
</SlideOver>
|
||||
)
|
||||
);
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import { useMutation } from "react-query";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { XIcon } from "@heroicons/react/solid";
|
||||
import { Field, FieldArray } from "formik";
|
||||
import { Field, FieldArray, FormikErrors, FormikValues } from "formik";
|
||||
import type { FieldProps } from "formik";
|
||||
|
||||
import { queryClient } from "../../App";
|
||||
|
@ -12,9 +12,9 @@ import {
|
|||
PasswordFieldWide,
|
||||
SwitchGroupWide,
|
||||
NumberFieldWide
|
||||
} from "../../components/inputs/input_wide";
|
||||
} from "../../components/inputs";
|
||||
import { SlideOver } from "../../components/panels";
|
||||
import Toast from '../../components/notifications/Toast';
|
||||
import Toast from "../../components/notifications/Toast";
|
||||
|
||||
interface ChannelsFieldArrayProps {
|
||||
channels: IrcChannel[];
|
||||
|
@ -95,35 +95,31 @@ interface IrcNetworkAddFormValues {
|
|||
channels: IrcChannel[];
|
||||
}
|
||||
|
||||
export function IrcNetworkAddForm({ isOpen, toggle }: any) {
|
||||
interface AddFormProps {
|
||||
isOpen: boolean;
|
||||
toggle: () => void;
|
||||
}
|
||||
|
||||
export function IrcNetworkAddForm({ isOpen, toggle }: AddFormProps) {
|
||||
const mutation = useMutation(
|
||||
(network: IrcNetwork) => APIClient.irc.createNetwork(network),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['networks']);
|
||||
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()
|
||||
queryClient.invalidateQueries(["networks"]);
|
||||
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();
|
||||
},
|
||||
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: any) => {
|
||||
// easy way to split textarea lines into array of strings for each newline.
|
||||
// parse on the field didn't really work.
|
||||
data.connect_commands = (
|
||||
data.connect_commands && data.connect_commands.length > 0 ?
|
||||
data.connect_commands.replace(/\r\n/g, "\n").split("\n") :
|
||||
[]
|
||||
);
|
||||
|
||||
mutation.mutate(data);
|
||||
const onSubmit = (data: unknown) => {
|
||||
mutation.mutate(data as IrcNetwork);
|
||||
};
|
||||
|
||||
const validate = (values: IrcNetworkAddFormValues) => {
|
||||
const errors = {} as any;
|
||||
const validate = (values: FormikValues) => {
|
||||
const errors = {} as FormikErrors<FormikValues>;
|
||||
if (!values.name)
|
||||
errors.name = "Required";
|
||||
|
||||
|
@ -137,7 +133,7 @@ export function IrcNetworkAddForm({ isOpen, toggle }: any) {
|
|||
errors.nickserv = { account: "Required" };
|
||||
|
||||
return errors;
|
||||
}
|
||||
};
|
||||
|
||||
const initialValues: IrcNetworkAddFormValues = {
|
||||
name: "",
|
||||
|
@ -149,7 +145,7 @@ export function IrcNetworkAddForm({ isOpen, toggle }: any) {
|
|||
nickserv: {
|
||||
account: ""
|
||||
},
|
||||
channels: [],
|
||||
channels: []
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -193,7 +189,7 @@ export function IrcNetworkAddForm({ isOpen, toggle }: any) {
|
|||
</>
|
||||
)}
|
||||
</SlideOver>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
interface IrcNetworkUpdateFormValues {
|
||||
|
@ -222,34 +218,27 @@ export function IrcNetworkUpdateForm({
|
|||
}: IrcNetworkUpdateFormProps) {
|
||||
const mutation = useMutation((network: IrcNetwork) => APIClient.irc.updateNetwork(network), {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['networks']);
|
||||
toast.custom((t) => <Toast type="success" body={`${network.name} was updated successfully`} t={t} />)
|
||||
toggle()
|
||||
queryClient.invalidateQueries(["networks"]);
|
||||
toast.custom((t) => <Toast type="success" body={`${network.name} was updated successfully`} t={t} />);
|
||||
toggle();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation((id: number) => APIClient.irc.deleteNetwork(id), {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['networks']);
|
||||
toast.custom((t) => <Toast type="success" body={`${network.name} was deleted.`} t={t} />)
|
||||
queryClient.invalidateQueries(["networks"]);
|
||||
toast.custom((t) => <Toast type="success" body={`${network.name} was deleted.`} t={t} />);
|
||||
|
||||
toggle()
|
||||
toggle();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const onSubmit = (data: any) => {
|
||||
// easy way to split textarea lines into array of strings for each newline.
|
||||
// parse on the field didn't really work.
|
||||
// TODO fix connect_commands on network update
|
||||
// let cmds = data.connect_commands && data.connect_commands.length > 0 ? data.connect_commands.replace(/\r\n/g,"\n").split("\n") : [];
|
||||
// data.connect_commands = cmds
|
||||
// console.log("formatted", data)
|
||||
|
||||
mutation.mutate(data)
|
||||
const onSubmit = (data: unknown) => {
|
||||
mutation.mutate(data as IrcNetwork);
|
||||
};
|
||||
|
||||
const validate = (values: any) => {
|
||||
const errors = {} as any;
|
||||
const validate = (values: FormikValues) => {
|
||||
const errors = {} as FormikErrors<FormikValues>;
|
||||
|
||||
if (!values.name) {
|
||||
errors.name = "Required";
|
||||
|
@ -264,15 +253,17 @@ export function IrcNetworkUpdateForm({
|
|||
}
|
||||
|
||||
if (!values.nickserv?.account) {
|
||||
errors.nickserv.account = "Required";
|
||||
errors.nickserv = {
|
||||
account: "Required"
|
||||
};
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteAction = () => {
|
||||
deleteMutation.mutate(network.id)
|
||||
}
|
||||
deleteMutation.mutate(network.id);
|
||||
};
|
||||
|
||||
const initialValues: IrcNetworkUpdateFormValues = {
|
||||
id: network.id,
|
||||
|
@ -285,7 +276,7 @@ export function IrcNetworkUpdateForm({
|
|||
pass: network.pass,
|
||||
channels: network.channels,
|
||||
invite_command: network.invite_command
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SlideOver
|
||||
|
@ -329,5 +320,5 @@ export function IrcNetworkUpdateForm({
|
|||
</>
|
||||
)}
|
||||
</SlideOver>
|
||||
)
|
||||
);
|
||||
}
|
|
@ -1,59 +1,63 @@
|
|||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { Fragment } from "react";
|
||||
import {Field, Form, Formik} from "formik";
|
||||
import type {FieldProps} from "formik";
|
||||
import {XIcon} from "@heroicons/react/solid";
|
||||
import Select, {components} from "react-select";
|
||||
import { Field, Form, Formik, FormikErrors, FormikValues } from "formik";
|
||||
import type { FieldProps } from "formik";
|
||||
import { XIcon } from "@heroicons/react/solid";
|
||||
import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select";
|
||||
import {
|
||||
SwitchGroupWide,
|
||||
TextFieldWide
|
||||
} from "../../components/inputs";
|
||||
import DEBUG from "../../components/debug";
|
||||
import {EventOptions, NotificationTypeOptions} from "../../domain/constants";
|
||||
import {useMutation} from "react-query";
|
||||
import {APIClient} from "../../api/APIClient";
|
||||
import {queryClient} from "../../App";
|
||||
import {toast} from "react-hot-toast";
|
||||
import { EventOptions, NotificationTypeOptions, SelectOption } from "../../domain/constants";
|
||||
import { useMutation } from "react-query";
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
import { queryClient } from "../../App";
|
||||
import { toast } from "react-hot-toast";
|
||||
import Toast from "../../components/notifications/Toast";
|
||||
import {SlideOver} from "../../components/panels";
|
||||
import { SlideOver } from "../../components/panels";
|
||||
import { componentMapType } from "./DownloadClientForms";
|
||||
|
||||
|
||||
const Input = (props: any) => {
|
||||
const Input = (props: InputProps) => {
|
||||
return (
|
||||
<components.Input
|
||||
{...props}
|
||||
inputClassName="outline-none border-none shadow-none focus:ring-transparent"
|
||||
className="text-gray-400 dark:text-gray-100"
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const Control = (props: any) => {
|
||||
const Control = (props: ControlProps) => {
|
||||
return (
|
||||
<components.Control
|
||||
{...props}
|
||||
className="p-1 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const Menu = (props: any) => {
|
||||
const Menu = (props: MenuProps) => {
|
||||
return (
|
||||
<components.Menu
|
||||
{...props}
|
||||
className="dark:bg-gray-800 border border-gray-300 dark:border-gray-700 dark:text-gray-400 rounded-md shadow-sm"
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const Option = (props: any) => {
|
||||
const Option = (props: OptionProps) => {
|
||||
return (
|
||||
<components.Option
|
||||
{...props}
|
||||
className="dark:text-gray-400 dark:bg-gray-800 dark:hover:bg-gray-900 dark:focus:bg-gray-900"
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function FormFieldsDiscord() {
|
||||
|
@ -76,8 +80,8 @@ function FormFieldsDiscord() {
|
|||
);
|
||||
}
|
||||
|
||||
const componentMap: any = {
|
||||
DISCORD: <FormFieldsDiscord/>,
|
||||
const componentMap: componentMapType = {
|
||||
DISCORD: <FormFieldsDiscord/>
|
||||
};
|
||||
|
||||
interface NotificationAddFormValues {
|
||||
|
@ -87,35 +91,35 @@ interface NotificationAddFormValues {
|
|||
|
||||
interface AddProps {
|
||||
isOpen: boolean;
|
||||
toggle: any;
|
||||
toggle: () => void;
|
||||
}
|
||||
|
||||
export function NotificationAddForm({isOpen, toggle}: AddProps) {
|
||||
export function NotificationAddForm({ isOpen, toggle }: AddProps) {
|
||||
const mutation = useMutation(
|
||||
(notification: Notification) => APIClient.notifications.create(notification),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['notifications']);
|
||||
toast.custom((t) => <Toast type="success" body="Notification added!" t={t} />)
|
||||
toggle()
|
||||
queryClient.invalidateQueries(["notifications"]);
|
||||
toast.custom((t) => <Toast type="success" body="Notification added!" t={t} />);
|
||||
toggle();
|
||||
},
|
||||
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: any) => {
|
||||
mutation.mutate(formData)
|
||||
}
|
||||
const onSubmit = (formData: unknown) => {
|
||||
mutation.mutate(formData as Notification);
|
||||
};
|
||||
|
||||
const validate = (values: NotificationAddFormValues) => {
|
||||
const errors = {} as any;
|
||||
const errors = {} as FormikErrors<FormikValues>;
|
||||
if (!values.name)
|
||||
errors.name = "Required";
|
||||
|
||||
return errors;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
|
@ -141,19 +145,20 @@ export function NotificationAddForm({isOpen, toggle}: AddProps) {
|
|||
type: "",
|
||||
name: "",
|
||||
webhook: "",
|
||||
events: [],
|
||||
events: []
|
||||
}}
|
||||
onSubmit={onSubmit}
|
||||
validate={validate}
|
||||
>
|
||||
{({values}) => (
|
||||
{({ values }) => (
|
||||
<Form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll">
|
||||
<div className="flex-1">
|
||||
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
|
||||
<div className="flex items-start justify-between space-x-3">
|
||||
<div className="space-y-1">
|
||||
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Add
|
||||
Notifications</Dialog.Title>
|
||||
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">
|
||||
Add Notifications
|
||||
</Dialog.Title>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-200">
|
||||
Trigger notifications on different events.
|
||||
</p>
|
||||
|
@ -187,12 +192,12 @@ export function NotificationAddForm({isOpen, toggle}: AddProps) {
|
|||
<Field name="type" type="select">
|
||||
{({
|
||||
field,
|
||||
form: {setFieldValue, resetForm}
|
||||
form: { setFieldValue, resetForm }
|
||||
}: FieldProps) => (
|
||||
<Select {...field}
|
||||
isClearable={true}
|
||||
isSearchable={true}
|
||||
components={{Input, Control, Menu, Option}}
|
||||
components={{ Input, Control, Menu, Option }}
|
||||
placeholder="Choose a type"
|
||||
styles={{
|
||||
singleValue: (base) => ({
|
||||
|
@ -205,14 +210,16 @@ export function NotificationAddForm({isOpen, toggle}: AddProps) {
|
|||
spacing: {
|
||||
...theme.spacing,
|
||||
controlHeight: 30,
|
||||
baseUnit: 2,
|
||||
baseUnit: 2
|
||||
}
|
||||
})}
|
||||
value={field?.value && field.value.value}
|
||||
onChange={(option: any) => {
|
||||
resetForm()
|
||||
onChange={(option: unknown) => {
|
||||
resetForm();
|
||||
|
||||
const opt = option as SelectOption;
|
||||
// setFieldValue("name", option?.label ?? "")
|
||||
setFieldValue(field.name, option?.value ?? "")
|
||||
setFieldValue(field.name, opt.value ?? "");
|
||||
}}
|
||||
options={NotificationTypeOptions}
|
||||
/>
|
||||
|
@ -273,7 +280,7 @@ export function NotificationAddForm({isOpen, toggle}: AddProps) {
|
|||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const EventCheckBoxes = () => (
|
||||
|
@ -303,23 +310,23 @@ const EventCheckBoxes = () => (
|
|||
</div>
|
||||
))}
|
||||
</fieldset>
|
||||
)
|
||||
);
|
||||
|
||||
interface UpdateProps {
|
||||
isOpen: boolean;
|
||||
toggle: any;
|
||||
toggle: () => void;
|
||||
notification: Notification;
|
||||
}
|
||||
|
||||
export function NotificationUpdateForm({isOpen, toggle, notification}: UpdateProps) {
|
||||
export function NotificationUpdateForm({ isOpen, toggle, notification }: UpdateProps) {
|
||||
const mutation = useMutation(
|
||||
(notification: Notification) => APIClient.notifications.update(notification),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["notifications"]);
|
||||
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();
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -328,14 +335,14 @@ export function NotificationUpdateForm({isOpen, toggle, notification}: UpdatePro
|
|||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["notifications"]);
|
||||
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: any) => {
|
||||
mutation.mutate(formData);
|
||||
}
|
||||
const onSubmit = (formData: unknown) => {
|
||||
mutation.mutate(formData as Notification);
|
||||
};
|
||||
|
||||
const deleteAction = () => {
|
||||
deleteMutation.mutate(notification.id);
|
||||
|
@ -347,8 +354,8 @@ export function NotificationUpdateForm({isOpen, toggle, notification}: UpdatePro
|
|||
type: notification.type,
|
||||
name: notification.name,
|
||||
webhook: notification.webhook,
|
||||
events: notification.events || [],
|
||||
}
|
||||
events: notification.events || []
|
||||
};
|
||||
|
||||
return (
|
||||
<SlideOver
|
||||
|
@ -376,11 +383,11 @@ export function NotificationUpdateForm({isOpen, toggle, notification}: UpdatePro
|
|||
</div>
|
||||
<div className="sm:col-span-2">
|
||||
<Field name="type" type="select">
|
||||
{({field, form: {setFieldValue, resetForm}}: FieldProps) => (
|
||||
{({ field, form: { setFieldValue, resetForm } }: FieldProps) => (
|
||||
<Select {...field}
|
||||
isClearable={true}
|
||||
isSearchable={true}
|
||||
components={{Input, Control, Menu, Option}}
|
||||
components={{ Input, Control, Menu, Option }}
|
||||
|
||||
placeholder="Choose a type"
|
||||
styles={{
|
||||
|
@ -394,13 +401,14 @@ export function NotificationUpdateForm({isOpen, toggle, notification}: UpdatePro
|
|||
spacing: {
|
||||
...theme.spacing,
|
||||
controlHeight: 30,
|
||||
baseUnit: 2,
|
||||
baseUnit: 2
|
||||
}
|
||||
})}
|
||||
value={field?.value && NotificationTypeOptions.find(o => o.value == field?.value)}
|
||||
onChange={(option: any) => {
|
||||
resetForm()
|
||||
setFieldValue(field.name, option?.value ?? "")
|
||||
onChange={(option: unknown) => {
|
||||
resetForm();
|
||||
const opt = option as SelectOption;
|
||||
setFieldValue(field.name, opt.value ?? "");
|
||||
}}
|
||||
options={NotificationTypeOptions}
|
||||
/>
|
||||
|
@ -431,5 +439,5 @@ export function NotificationUpdateForm({isOpen, toggle, notification}: UpdatePro
|
|||
</div>
|
||||
)}
|
||||
</SlideOver>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import type { ReportHandler } from "web-vitals";
|
|||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Fragment } from "react";
|
||||
import { NavLink, Link, Route, Switch } from "react-router-dom";
|
||||
import type { match } from "react-router-dom";
|
||||
import { Link, NavLink, Route, Switch } from "react-router-dom";
|
||||
import { Disclosure, Menu, Transition } from "@headlessui/react";
|
||||
import { ExternalLinkIcon } from "@heroicons/react/solid";
|
||||
import { ChevronDownIcon, MenuIcon, XIcon } from "@heroicons/react/outline";
|
||||
|
@ -11,9 +11,9 @@ import { Logs } from "./Logs";
|
|||
import { Releases } from "./releases";
|
||||
import { Dashboard } from "./dashboard";
|
||||
import { FilterDetails, Filters } from "./filters";
|
||||
import { AuthContext } from '../utils/Context';
|
||||
import { AuthContext } from "../utils/Context";
|
||||
|
||||
import logo from '../logo.png';
|
||||
import logo from "../logo.png";
|
||||
|
||||
interface NavItem {
|
||||
name: string;
|
||||
|
@ -21,11 +21,11 @@ interface NavItem {
|
|||
}
|
||||
|
||||
function classNames(...classes: string[]) {
|
||||
return classes.filter(Boolean).join(' ')
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
const isActiveMatcher = (
|
||||
match: match<any> | null,
|
||||
match: match | null,
|
||||
location: { pathname: string },
|
||||
item: NavItem
|
||||
) => {
|
||||
|
@ -33,20 +33,20 @@ const isActiveMatcher = (
|
|||
return false;
|
||||
|
||||
if (match?.url === "/" && item.path === "/" && location.pathname === "/")
|
||||
return true
|
||||
return true;
|
||||
|
||||
if (match.url === "/")
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
export default function Base() {
|
||||
const authContext = AuthContext.useValue();
|
||||
const nav: Array<NavItem> = [
|
||||
{ name: 'Dashboard', path: "/" },
|
||||
{ name: 'Filters', path: "/filters" },
|
||||
{ name: 'Releases', path: "/releases" },
|
||||
{ name: "Dashboard", path: "/" },
|
||||
{ name: "Filters", path: "/filters" },
|
||||
{ name: "Releases", path: "/releases" },
|
||||
{ name: "Settings", path: "/settings" },
|
||||
{ name: "Logs", path: "/logs" }
|
||||
];
|
||||
|
@ -102,7 +102,8 @@ export default function Base() {
|
|||
)}
|
||||
>
|
||||
Docs
|
||||
<ExternalLinkIcon className="inline ml-1 h-5 w-5" aria-hidden="true" />
|
||||
<ExternalLinkIcon className="inline ml-1 h-5 w-5"
|
||||
aria-hidden="true"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -148,8 +149,8 @@ export default function Base() {
|
|||
<Link
|
||||
to="/settings"
|
||||
className={classNames(
|
||||
active ? 'bg-gray-100 dark:bg-gray-600' : '',
|
||||
'block px-4 py-2 text-sm text-gray-700 dark:text-gray-200'
|
||||
active ? "bg-gray-100 dark:bg-gray-600" : "",
|
||||
"block px-4 py-2 text-sm text-gray-700 dark:text-gray-200"
|
||||
)}
|
||||
>
|
||||
Settings
|
||||
|
@ -161,8 +162,8 @@ export default function Base() {
|
|||
<Link
|
||||
to="/logout"
|
||||
className={classNames(
|
||||
active ? 'bg-gray-100 dark:bg-gray-600' : '',
|
||||
'block px-4 py-2 text-sm text-gray-700 dark:text-gray-200'
|
||||
active ? "bg-gray-100 dark:bg-gray-600" : "",
|
||||
"block px-4 py-2 text-sm text-gray-700 dark:text-gray-200"
|
||||
)}
|
||||
>
|
||||
Logout
|
||||
|
@ -182,9 +183,9 @@ export default function Base() {
|
|||
className="bg-gray-200 dark:bg-gray-800 inline-flex items-center justify-center p-2 rounded-md text-gray-600 dark:text-gray-400 hover:text-white hover:bg-gray-700">
|
||||
<span className="sr-only">Open main menu</span>
|
||||
{open ? (
|
||||
<XIcon className="block h-6 w-6" aria-hidden="true" />
|
||||
<XIcon className="block h-6 w-6" aria-hidden="true"/>
|
||||
) : (
|
||||
<MenuIcon className="block h-6 w-6" aria-hidden="true" />
|
||||
<MenuIcon className="block h-6 w-6" aria-hidden="true"/>
|
||||
)}
|
||||
</Disclosure.Button>
|
||||
</div>
|
||||
|
@ -221,29 +222,29 @@ export default function Base() {
|
|||
|
||||
<Switch>
|
||||
<Route path="/logs">
|
||||
<Logs />
|
||||
<Logs/>
|
||||
</Route>
|
||||
|
||||
<Route path="/settings">
|
||||
<Settings />
|
||||
<Settings/>
|
||||
</Route>
|
||||
|
||||
<Route path="/releases">
|
||||
<Releases />
|
||||
<Releases/>
|
||||
</Route>
|
||||
|
||||
<Route exact={true} path="/filters">
|
||||
<Filters />
|
||||
<Filters/>
|
||||
</Route>
|
||||
|
||||
<Route path="/filters/:filterId">
|
||||
<FilterDetails />
|
||||
<FilterDetails/>
|
||||
</Route>
|
||||
|
||||
<Route exact path="/">
|
||||
<Dashboard />
|
||||
<Dashboard/>
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ const LogColors: Record<LogLevel, string> = {
|
|||
"TRACE": "text-purple-300",
|
||||
"DEBUG": "text-yellow-500",
|
||||
"INFO": "text-green-500",
|
||||
"ERROR": "text-red-500",
|
||||
"ERROR": "text-red-500"
|
||||
};
|
||||
|
||||
export const Logs = () => {
|
||||
|
@ -29,7 +29,7 @@ export const Logs = () => {
|
|||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "auto" });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const es = APIClient.events.logs();
|
||||
|
@ -40,7 +40,7 @@ export const Logs = () => {
|
|||
|
||||
if (settings.scrollOnNewLog)
|
||||
scrollToBottom();
|
||||
}
|
||||
};
|
||||
|
||||
return () => es.close();
|
||||
}, [setLogs, settings]);
|
||||
|
@ -96,7 +96,7 @@ export const Logs = () => {
|
|||
key={idx}
|
||||
className={classNames(
|
||||
settings.indentLogLines ? "grid justify-start grid-flow-col" : "",
|
||||
settings.hideWrappedText ? "truncate hover:text-ellipsis hover:whitespace-normal" : "",
|
||||
settings.hideWrappedText ? "truncate hover:text-ellipsis hover:whitespace-normal" : ""
|
||||
)}
|
||||
>
|
||||
<span
|
||||
|
@ -112,7 +112,7 @@ export const Logs = () => {
|
|||
)}
|
||||
>
|
||||
{a.level}
|
||||
{' '}
|
||||
{" "}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="ml-2 text-black dark:text-gray-300">
|
||||
|
@ -125,5 +125,5 @@ export const Logs = () => {
|
|||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,36 +1,48 @@
|
|||
import {BellIcon, ChatAlt2Icon, CogIcon, CollectionIcon, DownloadIcon, KeyIcon, RssIcon} from '@heroicons/react/outline'
|
||||
import {NavLink, Route, Switch as RouteSwitch, useLocation, useRouteMatch} from "react-router-dom";
|
||||
import { BellIcon, ChatAlt2Icon, CogIcon, CollectionIcon, DownloadIcon, KeyIcon, RssIcon } from "@heroicons/react/outline";
|
||||
import { NavLink, Route, Switch as RouteSwitch, useLocation, useRouteMatch } from "react-router-dom";
|
||||
|
||||
import { classNames } from "../utils";
|
||||
import IndexerSettings from "./settings/Indexer";
|
||||
import { IrcSettings } from "./settings/Irc";
|
||||
import ApplicationSettings from "./settings/Application";
|
||||
import DownloadClientSettings from "./settings/DownloadClient";
|
||||
import { RegexPlayground } from './settings/RegexPlayground';
|
||||
import { RegexPlayground } from "./settings/RegexPlayground";
|
||||
import ReleaseSettings from "./settings/Releases";
|
||||
import NotificationSettings from "./settings/Notifications";
|
||||
import FeedSettings from "./settings/Feed";
|
||||
|
||||
const subNavigation = [
|
||||
{name: 'Application', href: '', icon: CogIcon, current: true},
|
||||
{name: 'Indexers', href: 'indexers', icon: KeyIcon, current: false},
|
||||
{name: 'IRC', href: 'irc', icon: ChatAlt2Icon, current: false},
|
||||
{name: 'Feeds', href: 'feeds', icon: RssIcon, current: false},
|
||||
{name: 'Clients', href: 'clients', icon: DownloadIcon, current: false},
|
||||
{name: 'Notifications', href: 'notifications', icon: BellIcon, current: false},
|
||||
{name: 'Releases', href: 'releases', icon: CollectionIcon, current: false},
|
||||
interface NavTabType {
|
||||
name: string;
|
||||
href: string;
|
||||
icon: typeof CogIcon;
|
||||
current: boolean;
|
||||
}
|
||||
|
||||
const subNavigation: NavTabType[] = [
|
||||
{ name: "Application", href: "", icon: CogIcon, current: true },
|
||||
{ name: "Indexers", href: "indexers", icon: KeyIcon, current: false },
|
||||
{ name: "IRC", href: "irc", icon: ChatAlt2Icon, current: false },
|
||||
{ name: "Feeds", href: "feeds", icon: RssIcon, current: false },
|
||||
{ name: "Clients", href: "clients", icon: DownloadIcon, current: false },
|
||||
{ name: "Notifications", href: "notifications", icon: BellIcon, current: false },
|
||||
{ name: "Releases", href: "releases", icon: CollectionIcon, current: false }
|
||||
// {name: 'Regex Playground', href: 'regex-playground', icon: CogIcon, current: false}
|
||||
// {name: 'Rules', href: 'rules', icon: ClipboardCheckIcon, current: false},
|
||||
]
|
||||
];
|
||||
|
||||
function SubNavLink({item, url}: any) {
|
||||
interface NavLinkProps {
|
||||
item: NavTabType;
|
||||
url: string;
|
||||
}
|
||||
|
||||
function SubNavLink({ item, url }: NavLinkProps) {
|
||||
const location = useLocation();
|
||||
const { pathname } = location;
|
||||
|
||||
const splitLocation = pathname.split("/");
|
||||
|
||||
// we need to clean the / if it's a base root path
|
||||
const too = item.href ? `${url}/${item.href}` : url
|
||||
const too = item.href ? `${url}/${item.href}` : url;
|
||||
return (
|
||||
<NavLink
|
||||
key={item.name}
|
||||
|
@ -38,9 +50,9 @@ function SubNavLink({item, url}: any) {
|
|||
exact={true}
|
||||
activeClassName="bg-teal-50 dark:bg-gray-700 border-teal-500 dark:border-blue-500 text-teal-700 dark:text-white hover:bg-teal-50 dark:hover:bg-gray-500 hover:text-teal-700 dark:hover:text-gray-200"
|
||||
className={classNames(
|
||||
'border-transparent text-gray-900 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-300 group border-l-4 px-3 py-2 flex items-center text-sm font-medium'
|
||||
"border-transparent text-gray-900 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-300 group border-l-4 px-3 py-2 flex items-center text-sm font-medium"
|
||||
)}
|
||||
aria-current={splitLocation[2] === item.href ? 'page' : undefined}
|
||||
aria-current={splitLocation[2] === item.href ? "page" : undefined}
|
||||
>
|
||||
<item.icon
|
||||
className="text-gray-400 group-hover:text-gray-500 dark:group-hover:text-gray-300 flex-shrink-0 -ml-1 mr-3 h-6 w-6"
|
||||
|
@ -48,19 +60,24 @@ function SubNavLink({item, url}: any) {
|
|||
/>
|
||||
<span className="truncate">{item.name}</span>
|
||||
</NavLink>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarNav({subNavigation, url}: any) {
|
||||
interface SidebarNavProps {
|
||||
subNavigation: NavTabType[];
|
||||
url: string;
|
||||
}
|
||||
|
||||
function SidebarNav({ subNavigation, url }: SidebarNavProps) {
|
||||
return (
|
||||
<aside className="py-2 lg:col-span-3">
|
||||
<nav className="space-y-1">
|
||||
{subNavigation.map((item: any) => (
|
||||
{subNavigation.map((item) => (
|
||||
<SubNavLink item={item} url={url} key={item.href}/>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default function Settings() {
|
||||
|
@ -115,6 +132,6 @@ export default function Settings() {
|
|||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -34,11 +34,11 @@ export const Login = () => {
|
|||
isLoggedIn: true
|
||||
});
|
||||
history.push("/");
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const handleSubmit = (data: any) => mutation.mutate(data);
|
||||
const handleSubmit = (data: LoginData) => mutation.mutate(data);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||
|
@ -75,4 +75,4 @@ export const Login = () => {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -29,4 +29,4 @@ export const Logout = () => {
|
|||
<p>Logged out</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -37,7 +37,7 @@ export const Onboarding = () => {
|
|||
{
|
||||
onSuccess: () => {
|
||||
history.push("/login");
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -81,5 +81,5 @@ export const Onboarding = () => {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
useFilters,
|
||||
useGlobalFilter,
|
||||
useSortBy,
|
||||
usePagination
|
||||
usePagination, FilterProps, Column
|
||||
} from "react-table";
|
||||
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
|
@ -17,17 +17,17 @@ import * as DataTable from "../../components/data-table";
|
|||
// This is a custom filter UI for selecting
|
||||
// a unique option from a list
|
||||
function SelectColumnFilter({
|
||||
column: { filterValue, setFilter, preFilteredRows, id, render },
|
||||
}: any) {
|
||||
column: { filterValue, setFilter, preFilteredRows, id, render }
|
||||
}: FilterProps<object>) {
|
||||
// Calculate the options for filtering
|
||||
// using the preFilteredRows
|
||||
const options = React.useMemo(() => {
|
||||
const options: any = new Set()
|
||||
preFilteredRows.forEach((row: { values: { [x: string]: unknown } }) => {
|
||||
options.add(row.values[id])
|
||||
})
|
||||
return [...options.values()]
|
||||
}, [id, preFilteredRows])
|
||||
const options = new Set<string>();
|
||||
preFilteredRows.forEach((row: { values: { [x: string]: string } }) => {
|
||||
options.add(row.values[id]);
|
||||
});
|
||||
return [...options.values()];
|
||||
}, [id, preFilteredRows]);
|
||||
|
||||
// Render a multi-select box
|
||||
return (
|
||||
|
@ -39,7 +39,7 @@ function SelectColumnFilter({
|
|||
id={id}
|
||||
value={filterValue}
|
||||
onChange={e => {
|
||||
setFilter(e.target.value || undefined)
|
||||
setFilter(e.target.value || undefined);
|
||||
}}
|
||||
>
|
||||
<option value="">All</option>
|
||||
|
@ -50,17 +50,22 @@ function SelectColumnFilter({
|
|||
))}
|
||||
</select>
|
||||
</label>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function Table({ columns, data }: any) {
|
||||
interface TableProps {
|
||||
columns: Column[];
|
||||
data: Release[];
|
||||
}
|
||||
|
||||
function Table({ columns, data }: TableProps) {
|
||||
// Use the state and functions returned from useTable to build your UI
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
prepareRow,
|
||||
page, // Instead of using 'rows', we'll use page,
|
||||
page // Instead of using 'rows', we'll use page,
|
||||
} = useTable(
|
||||
{ columns, data },
|
||||
useFilters,
|
||||
|
@ -94,7 +99,7 @@ function Table({ columns, data }: any) {
|
|||
{...columnRest}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
{column.render('Header')}
|
||||
{column.render("Header")}
|
||||
{/* Add a sort direction indicator */}
|
||||
<span>
|
||||
{column.isSorted ? (
|
||||
|
@ -119,12 +124,12 @@ function Table({ columns, data }: any) {
|
|||
{...getTableBodyProps()}
|
||||
className="divide-y divide-gray-200 dark:divide-gray-700"
|
||||
>
|
||||
{page.map((row: any) => {
|
||||
{page.map((row) => {
|
||||
prepareRow(row);
|
||||
const { key: bodyRowKey, ...bodyRowRest } = row.getRowProps();
|
||||
return (
|
||||
<tr key={bodyRowKey} {...bodyRowRest}>
|
||||
{row.cells.map((cell: any) => {
|
||||
{row.cells.map((cell) => {
|
||||
const { key: cellRowKey, ...cellRowRest } = cell.getCellProps();
|
||||
return (
|
||||
<td
|
||||
|
@ -133,12 +138,12 @@ function Table({ columns, data }: any) {
|
|||
role="cell"
|
||||
{...cellRowRest}
|
||||
>
|
||||
{cell.render('Cell')}
|
||||
{cell.render("Cell")}
|
||||
</td>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -151,30 +156,30 @@ export const ActivityTable = () => {
|
|||
const columns = React.useMemo(() => [
|
||||
{
|
||||
Header: "Age",
|
||||
accessor: 'timestamp',
|
||||
Cell: DataTable.AgeCell,
|
||||
accessor: "timestamp",
|
||||
Cell: DataTable.AgeCell
|
||||
},
|
||||
{
|
||||
Header: "Release",
|
||||
accessor: 'torrent_name',
|
||||
Cell: DataTable.TitleCell,
|
||||
accessor: "torrent_name",
|
||||
Cell: DataTable.TitleCell
|
||||
},
|
||||
{
|
||||
Header: "Actions",
|
||||
accessor: 'action_status',
|
||||
Cell: DataTable.ReleaseStatusCell,
|
||||
accessor: "action_status",
|
||||
Cell: DataTable.ReleaseStatusCell
|
||||
},
|
||||
{
|
||||
Header: "Indexer",
|
||||
accessor: 'indexer',
|
||||
accessor: "indexer",
|
||||
Cell: DataTable.TitleCell,
|
||||
Filter: SelectColumnFilter,
|
||||
filter: 'includes',
|
||||
},
|
||||
], [])
|
||||
filter: "includes"
|
||||
}
|
||||
], []);
|
||||
|
||||
const { isLoading, data } = useQuery(
|
||||
'dash_release',
|
||||
"dash_release",
|
||||
() => APIClient.release.find("?limit=10"),
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
|
@ -188,7 +193,7 @@ export const ActivityTable = () => {
|
|||
Recent activity
|
||||
</h3>
|
||||
|
||||
<Table columns={columns} data={data?.data} />
|
||||
<Table columns={columns} data={data?.data ?? []} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -19,7 +19,7 @@ const StatsItem = ({ name, value }: StatsItemProps) => (
|
|||
<p className="text-3xl font-extrabold text-gray-900 dark:text-gray-200">{value}</p>
|
||||
</dd>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
export const Stats = () => {
|
||||
const { isLoading, data } = useQuery(
|
||||
|
@ -44,5 +44,5 @@ export const Stats = () => {
|
|||
<StatsItem name="Approved Pushes" value={data?.push_approved_count} />
|
||||
</dl>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
import { Fragment, useRef } from "react";
|
||||
import React, { Fragment, useRef } from "react";
|
||||
import { useMutation, useQuery } from "react-query";
|
||||
import {
|
||||
NavLink,
|
||||
|
@ -10,10 +10,9 @@ import {
|
|||
useRouteMatch
|
||||
} from "react-router-dom";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { Field, FieldArray, Form, Formik } from "formik";
|
||||
import { Field, FieldArray, FieldProps, Form, Formik, FormikValues } from "formik";
|
||||
import { Dialog, Transition, Switch as SwitchBasic } from "@headlessui/react";
|
||||
import { ChevronDownIcon, ChevronRightIcon, } from "@heroicons/react/solid";
|
||||
|
||||
import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/solid";
|
||||
|
||||
import {
|
||||
CONTAINER_OPTIONS,
|
||||
|
@ -50,16 +49,27 @@ import { DeleteModal } from "../../components/modals";
|
|||
import { TitleSubtitle } from "../../components/headings";
|
||||
import { EmptyListState } from "../../components/emptystates";
|
||||
|
||||
const tabs = [
|
||||
{ name: 'General', href: '', current: true },
|
||||
{ name: 'Movies and TV', href: 'movies-tv', current: false },
|
||||
{ name: 'Music', href: 'music', current: false },
|
||||
// { name: 'P2P', href: 'p2p', current: false },
|
||||
{ name: 'Advanced', href: 'advanced', current: false },
|
||||
{ name: 'Actions', href: 'actions', current: false },
|
||||
]
|
||||
interface tabType {
|
||||
name: string;
|
||||
href: string;
|
||||
current: boolean;
|
||||
}
|
||||
|
||||
function TabNavLink({ item, url }: any) {
|
||||
const tabs: tabType[] = [
|
||||
{ name: "General", href: "", current: true },
|
||||
{ name: "Movies and TV", href: "movies-tv", current: false },
|
||||
{ name: "Music", href: "music", current: false },
|
||||
// { name: 'P2P', href: 'p2p', current: false },
|
||||
{ name: "Advanced", href: "advanced", current: false },
|
||||
{ name: "Actions", href: "actions", current: false }
|
||||
];
|
||||
|
||||
export interface NavLinkProps {
|
||||
item: tabType;
|
||||
url: string;
|
||||
}
|
||||
|
||||
function TabNavLink({ item, url }: NavLinkProps) {
|
||||
const location = useLocation();
|
||||
const splitLocation = location.pathname.split("/");
|
||||
|
||||
|
@ -71,16 +81,23 @@ function TabNavLink({ item, url }: any) {
|
|||
exact
|
||||
activeClassName="border-purple-600 dark:border-blue-500 text-purple-600 dark:text-white"
|
||||
className={classNames(
|
||||
'border-transparent text-gray-500 hover:text-purple-600 dark:hover:text-white hover:border-purple-600 dark:hover:border-blue-500 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm'
|
||||
"border-transparent text-gray-500 hover:text-purple-600 dark:hover:text-white hover:border-purple-600 dark:hover:border-blue-500 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm"
|
||||
)}
|
||||
aria-current={splitLocation[2] === item.href ? 'page' : undefined}
|
||||
aria-current={splitLocation[2] === item.href ? "page" : undefined}
|
||||
>
|
||||
{item.name}
|
||||
</NavLink>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const FormButtonsGroup = ({ values, deleteAction, reset }: any) => {
|
||||
interface FormButtonsGroupProps {
|
||||
values: FormikValues;
|
||||
deleteAction: () => void;
|
||||
reset: () => void;
|
||||
dirty?: boolean;
|
||||
}
|
||||
|
||||
const FormButtonsGroup = ({ values, deleteAction, reset }: FormButtonsGroupProps) => {
|
||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
||||
|
||||
const cancelModalButtonRef = useRef(null);
|
||||
|
@ -123,8 +140,8 @@ const FormButtonsGroup = ({ values, deleteAction, reset }: any) => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default function FilterDetails() {
|
||||
const history = useHistory();
|
||||
|
@ -164,16 +181,16 @@ export default function FilterDetails() {
|
|||
queryClient.invalidateQueries(["filters"]);
|
||||
|
||||
// redirect
|
||||
history.push("/filters")
|
||||
history.push("/filters");
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!filter) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleSubmit = (data: Filter) => {
|
||||
|
@ -181,17 +198,17 @@ export default function FilterDetails() {
|
|||
// TODO add options for these
|
||||
data.actions.forEach((a: Action) => {
|
||||
if (a.type === "WEBHOOK") {
|
||||
a.webhook_method = "POST"
|
||||
a.webhook_type = "JSON"
|
||||
a.webhook_method = "POST";
|
||||
a.webhook_type = "JSON";
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
updateMutation.mutate(data)
|
||||
}
|
||||
updateMutation.mutate(data);
|
||||
};
|
||||
|
||||
const deleteAction = () => {
|
||||
deleteMutation.mutate(filter.id)
|
||||
}
|
||||
deleteMutation.mutate(filter.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<main>
|
||||
|
@ -267,7 +284,7 @@ export default function FilterDetails() {
|
|||
albums: filter.albums,
|
||||
origins: filter.origins || [],
|
||||
indexers: filter.indexers || [],
|
||||
actions: filter.actions || [],
|
||||
actions: filter.actions || []
|
||||
} as Filter}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
|
@ -308,7 +325,7 @@ export default function FilterDetails() {
|
|||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function General() {
|
||||
|
@ -320,12 +337,13 @@ function General() {
|
|||
|
||||
const opts = indexers && indexers.length > 0 ? indexers.map(v => ({
|
||||
label: v.name,
|
||||
value: {
|
||||
id: v.id,
|
||||
name: v.name,
|
||||
identifier: v.identifier,
|
||||
enabled: v.enabled
|
||||
}
|
||||
value: v.id
|
||||
// value: {
|
||||
// id: v.id,
|
||||
// name: v.name,
|
||||
// identifier: v.identifier,
|
||||
// enabled: v.enabled
|
||||
// }
|
||||
})) : [];
|
||||
|
||||
return (
|
||||
|
@ -401,7 +419,7 @@ function MoviesTv() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function Music() {
|
||||
|
@ -453,7 +471,7 @@ function Music() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function Advanced() {
|
||||
|
@ -497,19 +515,19 @@ function Advanced() {
|
|||
<TextField name="freeleech_percent" label="Freeleech percent" columns={6} />
|
||||
</CollapsableSection>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
interface CollapsableSectionProps {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
children: any;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function CollapsableSection({ title, subtitle, children }: CollapsableSectionProps) {
|
||||
const [isOpen, toggleOpen] = useToggle(false)
|
||||
const [isOpen, toggleOpen] = useToggle(false);
|
||||
|
||||
return(
|
||||
return (
|
||||
<div className="mt-6 lg:pb-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex justify-between items-center cursor-pointer" onClick={toggleOpen}>
|
||||
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
||||
|
@ -531,12 +549,12 @@ function CollapsableSection({ title, subtitle, children }: CollapsableSectionPro
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
interface FilterActionsProps {
|
||||
filter: Filter;
|
||||
values: any;
|
||||
values: FormikValues;
|
||||
}
|
||||
|
||||
function FilterActions({ filter, values }: FilterActionsProps) {
|
||||
|
@ -572,9 +590,9 @@ function FilterActions({ filter, values }: FilterActionsProps) {
|
|||
webhook_type: "",
|
||||
webhook_method: "",
|
||||
webhook_data: "",
|
||||
webhook_headers: [],
|
||||
webhook_headers: []
|
||||
// client_id: 0,
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-10">
|
||||
|
@ -602,8 +620,8 @@ function FilterActions({ filter, values }: FilterActionsProps) {
|
|||
<div className="light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
|
||||
{values.actions.length > 0 ?
|
||||
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{values.actions.map((action: any, index: number) => (
|
||||
<FilterActionsItem action={action} clients={data!} idx={index} remove={remove} key={index} />
|
||||
{values.actions.map((action: Action, index: number) => (
|
||||
<FilterActionsItem action={action} clients={data ?? []} idx={index} remove={remove} key={index} />
|
||||
))}
|
||||
</ul>
|
||||
: <EmptyListState text="No actions yet!" />
|
||||
|
@ -613,14 +631,14 @@ function FilterActions({ filter, values }: FilterActionsProps) {
|
|||
)}
|
||||
</FieldArray>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
interface FilterActionsItemProps {
|
||||
action: Action;
|
||||
clients: DownloadClient[];
|
||||
idx: number;
|
||||
remove: any;
|
||||
remove: <T>(index: number) => T | undefined;
|
||||
}
|
||||
|
||||
function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemProps) {
|
||||
|
@ -681,7 +699,7 @@ function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemPr
|
|||
name={`actions.${idx}.webhook_data`}
|
||||
label="Data (json)"
|
||||
columns={6}
|
||||
placeholder={`Request data: { "key": "value" }`}
|
||||
placeholder={"Request data: { \"key\": \"value\" }"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -856,27 +874,27 @@ function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemPr
|
|||
<Field name={`actions.${idx}.enabled`} type="checkbox">
|
||||
{({
|
||||
field,
|
||||
form: { setFieldValue },
|
||||
}: any) => (
|
||||
form: { setFieldValue }
|
||||
}: FieldProps) => (
|
||||
<SwitchBasic
|
||||
{...field}
|
||||
type="button"
|
||||
value={field.value}
|
||||
checked={field.checked}
|
||||
onChange={value => {
|
||||
setFieldValue(field?.name ?? '', value)
|
||||
checked={field.checked ?? false}
|
||||
onChange={(value: boolean) => {
|
||||
setFieldValue(field?.name ?? "", value);
|
||||
}}
|
||||
className={classNames(
|
||||
field.value ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600',
|
||||
'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'
|
||||
field.value ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-600",
|
||||
"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"
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">toggle enabled</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
field.value ? 'translate-x-5' : 'translate-x-0',
|
||||
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
||||
field.value ? "translate-x-5" : "translate-x-0",
|
||||
"inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
|
||||
)}
|
||||
/>
|
||||
</SwitchBasic>
|
||||
|
@ -972,5 +990,5 @@ function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemPr
|
|||
</div>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
TrashIcon,
|
||||
PencilAltIcon,
|
||||
SwitchHorizontalIcon,
|
||||
DotsHorizontalIcon, DuplicateIcon,
|
||||
DotsHorizontalIcon, DuplicateIcon
|
||||
} from "@heroicons/react/outline";
|
||||
|
||||
import { queryClient } from "../../App";
|
||||
|
@ -20,7 +20,7 @@ import { EmptyListState } from "../../components/emptystates";
|
|||
import { DeleteModal } from "../../components/modals";
|
||||
|
||||
export default function Filters() {
|
||||
const [createFilterIsOpen, toggleCreateFilter] = useToggle(false)
|
||||
const [createFilterIsOpen, toggleCreateFilter] = useToggle(false);
|
||||
|
||||
const { isLoading, error, data } = useQuery(
|
||||
["filters"],
|
||||
|
@ -63,7 +63,7 @@ export default function Filters() {
|
|||
)}
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
interface FilterListProps {
|
||||
|
@ -97,7 +97,7 @@ function FilterList({ filters }: FilterListProps) {
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
interface FilterItemDropdownProps {
|
||||
|
@ -256,7 +256,7 @@ const FilterItemDropdown = ({
|
|||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
interface FilterListItemProps {
|
||||
filter: Filter;
|
||||
|
@ -264,13 +264,13 @@ interface FilterListItemProps {
|
|||
}
|
||||
|
||||
function FilterListItem({ filter, idx }: FilterListItemProps) {
|
||||
const [enabled, setEnabled] = useState(filter.enabled)
|
||||
const [enabled, setEnabled] = useState(filter.enabled);
|
||||
|
||||
const updateMutation = useMutation(
|
||||
(status: boolean) => APIClient.filters.toggleEnable(filter.id, status),
|
||||
{
|
||||
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 ${enabled ? "disabled" : "enabled"} successfully`} t={t} />);
|
||||
|
||||
// We need to invalidate both keys here.
|
||||
// The filters key is used on the /filters page,
|
||||
|
@ -284,7 +284,7 @@ function FilterListItem({ filter, idx }: FilterListItemProps) {
|
|||
const toggleActive = (status: boolean) => {
|
||||
setEnabled(status);
|
||||
updateMutation.mutate(status);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<tr
|
||||
|
@ -303,16 +303,16 @@ function FilterListItem({ filter, idx }: FilterListItemProps) {
|
|||
checked={enabled}
|
||||
onChange={toggleActive}
|
||||
className={classNames(
|
||||
enabled ? 'bg-teal-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'
|
||||
enabled ? "bg-teal-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"
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
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'
|
||||
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"
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
|
@ -342,5 +342,5 @@ function FilterListItem({ filter, idx }: FilterListItemProps) {
|
|||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
);
|
||||
}
|
|
@ -3,19 +3,20 @@ import { useQuery } from "react-query";
|
|||
import { Listbox, Transition } from "@headlessui/react";
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronDownIcon
|
||||
} from "@heroicons/react/solid";
|
||||
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
import { classNames } from "../../utils";
|
||||
import { PushStatusOptions } from "../../domain/constants";
|
||||
import { FilterProps } from "react-table";
|
||||
|
||||
interface ListboxFilterProps {
|
||||
id: string;
|
||||
label: string;
|
||||
currentValue: string;
|
||||
onChange: (newValue: string) => void;
|
||||
children: any;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const ListboxFilter = ({
|
||||
|
@ -62,13 +63,13 @@ const ListboxFilter = ({
|
|||
// a unique option from a list
|
||||
export const IndexerSelectColumnFilter = ({
|
||||
column: { filterValue, setFilter, id }
|
||||
}: any) => {
|
||||
}: FilterProps<object>) => {
|
||||
const { data, isSuccess } = useQuery(
|
||||
"release_indexers",
|
||||
() => APIClient.release.indexerOptions(),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
staleTime: Infinity,
|
||||
staleTime: Infinity
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -84,8 +85,8 @@ export const IndexerSelectColumnFilter = ({
|
|||
<FilterOption key={idx} label={indexer} value={indexer} />
|
||||
))}
|
||||
</ListboxFilter>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
interface FilterOptionProps {
|
||||
label: string;
|
||||
|
@ -96,7 +97,7 @@ const FilterOption = ({ label, value }: FilterOptionProps) => (
|
|||
<Listbox.Option
|
||||
className={({ active }) => classNames(
|
||||
"cursor-pointer select-none relative py-2 pl-10 pr-4",
|
||||
active ? 'text-black dark:text-gray-200 bg-gray-100 dark:bg-gray-900' : 'text-gray-700 dark:text-gray-400'
|
||||
active ? "text-black dark:text-gray-200 bg-gray-100 dark:bg-gray-900" : "text-gray-700 dark:text-gray-400"
|
||||
)}
|
||||
value={value}
|
||||
>
|
||||
|
@ -122,15 +123,13 @@ const FilterOption = ({ label, value }: FilterOptionProps) => (
|
|||
|
||||
export const PushStatusSelectColumnFilter = ({
|
||||
column: { filterValue, setFilter, id }
|
||||
}: any) => (
|
||||
}: FilterProps<object>) => {
|
||||
const label = filterValue ? PushStatusOptions.find((o) => o.value === filterValue && o.value)?.label : "Push status";
|
||||
return (
|
||||
<div className="mr-3">
|
||||
<ListboxFilter
|
||||
id={id}
|
||||
label={
|
||||
filterValue
|
||||
? PushStatusOptions.find((o) => o.value === filterValue && o.value)?.label
|
||||
: "Push status"
|
||||
}
|
||||
label={label ?? "Push status"}
|
||||
currentValue={filterValue}
|
||||
onChange={setFilter}
|
||||
>
|
||||
|
@ -139,4 +138,4 @@ export const PushStatusSelectColumnFilter = ({
|
|||
))}
|
||||
</ListboxFilter>
|
||||
</div>
|
||||
);
|
||||
);};
|
|
@ -25,30 +25,45 @@ import {
|
|||
PushStatusSelectColumnFilter
|
||||
} from "./Filters";
|
||||
|
||||
const initialState = {
|
||||
type TableState = {
|
||||
queryPageIndex: number;
|
||||
queryPageSize: number;
|
||||
totalCount: number;
|
||||
queryFilters: ReleaseFilter[];
|
||||
};
|
||||
|
||||
const initialState: TableState = {
|
||||
queryPageIndex: 0,
|
||||
queryPageSize: 10,
|
||||
totalCount: null,
|
||||
totalCount: 0,
|
||||
queryFilters: []
|
||||
};
|
||||
|
||||
const PAGE_CHANGED = 'PAGE_CHANGED';
|
||||
const PAGE_SIZE_CHANGED = 'PAGE_SIZE_CHANGED';
|
||||
const TOTAL_COUNT_CHANGED = 'TOTAL_COUNT_CHANGED';
|
||||
const FILTER_CHANGED = 'FILTER_CHANGED';
|
||||
enum ActionType {
|
||||
PAGE_CHANGED = "PAGE_CHANGED",
|
||||
PAGE_SIZE_CHANGED = "PAGE_SIZE_CHANGED",
|
||||
TOTAL_COUNT_CHANGED = "TOTAL_COUNT_CHANGED",
|
||||
FILTER_CHANGED = "FILTER_CHANGED"
|
||||
}
|
||||
|
||||
const TableReducer = (state: any, { type, payload }: any) => {
|
||||
switch (type) {
|
||||
case PAGE_CHANGED:
|
||||
return { ...state, queryPageIndex: payload };
|
||||
case PAGE_SIZE_CHANGED:
|
||||
return { ...state, queryPageSize: payload };
|
||||
case TOTAL_COUNT_CHANGED:
|
||||
return { ...state, totalCount: payload };
|
||||
case FILTER_CHANGED:
|
||||
return { ...state, queryFilters: payload };
|
||||
type Actions =
|
||||
| { type: ActionType.FILTER_CHANGED; payload: ReleaseFilter[]; }
|
||||
| { type: ActionType.PAGE_CHANGED; payload: number; }
|
||||
| { type: ActionType.PAGE_SIZE_CHANGED; payload: number; }
|
||||
| { type: ActionType.TOTAL_COUNT_CHANGED; payload: number; };
|
||||
|
||||
const TableReducer = (state: TableState, action: Actions): TableState => {
|
||||
switch (action.type) {
|
||||
case ActionType.PAGE_CHANGED:
|
||||
return { ...state, queryPageIndex: action.payload };
|
||||
case ActionType.PAGE_SIZE_CHANGED:
|
||||
return { ...state, queryPageSize: action.payload };
|
||||
case ActionType.FILTER_CHANGED:
|
||||
return { ...state, queryFilters: action.payload };
|
||||
case ActionType.TOTAL_COUNT_CHANGED:
|
||||
return { ...state, totalCount: action.payload };
|
||||
default:
|
||||
throw new Error(`Unhandled action type: ${type}`);
|
||||
throw new Error(`Unhandled action type: ${action}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -56,38 +71,38 @@ export const ReleaseTable = () => {
|
|||
const columns = React.useMemo(() => [
|
||||
{
|
||||
Header: "Age",
|
||||
accessor: 'timestamp',
|
||||
Cell: DataTable.AgeCell,
|
||||
accessor: "timestamp",
|
||||
Cell: DataTable.AgeCell
|
||||
},
|
||||
{
|
||||
Header: "Release",
|
||||
accessor: 'torrent_name',
|
||||
Cell: DataTable.TitleCell,
|
||||
accessor: "torrent_name",
|
||||
Cell: DataTable.TitleCell
|
||||
},
|
||||
{
|
||||
Header: "Actions",
|
||||
accessor: 'action_status',
|
||||
accessor: "action_status",
|
||||
Cell: DataTable.ReleaseStatusCell,
|
||||
Filter: PushStatusSelectColumnFilter,
|
||||
Filter: PushStatusSelectColumnFilter
|
||||
},
|
||||
{
|
||||
Header: "Indexer",
|
||||
accessor: 'indexer',
|
||||
accessor: "indexer",
|
||||
Cell: DataTable.TitleCell,
|
||||
Filter: IndexerSelectColumnFilter,
|
||||
filter: 'equal',
|
||||
},
|
||||
] as Column<Release>[], [])
|
||||
filter: "equal"
|
||||
}
|
||||
] as Column<Release>[], []);
|
||||
|
||||
const [{ queryPageIndex, queryPageSize, totalCount, queryFilters }, dispatch] =
|
||||
React.useReducer(TableReducer, initialState);
|
||||
|
||||
const { isLoading, error, data, isSuccess } = useQuery(
|
||||
['releases', queryPageIndex, queryPageSize, queryFilters],
|
||||
["releases", queryPageIndex, queryPageSize, queryFilters],
|
||||
() => APIClient.release.findQuery(queryPageIndex * queryPageSize, queryPageSize, queryFilters),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
staleTime: 5000,
|
||||
staleTime: 5000
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -129,29 +144,29 @@ export const ReleaseTable = () => {
|
|||
},
|
||||
useFilters,
|
||||
useSortBy,
|
||||
usePagination,
|
||||
usePagination
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
dispatch({ type: PAGE_CHANGED, payload: pageIndex });
|
||||
dispatch({ type: ActionType.PAGE_CHANGED, payload: pageIndex });
|
||||
}, [pageIndex]);
|
||||
|
||||
React.useEffect(() => {
|
||||
dispatch({ type: PAGE_SIZE_CHANGED, payload: pageSize });
|
||||
dispatch({ type: ActionType.PAGE_SIZE_CHANGED, payload: pageSize });
|
||||
gotoPage(0);
|
||||
}, [pageSize, gotoPage]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (data?.count) {
|
||||
dispatch({
|
||||
type: TOTAL_COUNT_CHANGED,
|
||||
payload: data.count,
|
||||
type: ActionType.TOTAL_COUNT_CHANGED,
|
||||
payload: data.count
|
||||
});
|
||||
}
|
||||
}, [data?.count]);
|
||||
|
||||
React.useEffect(() => {
|
||||
dispatch({ type: FILTER_CHANGED, payload: filters });
|
||||
dispatch({ type: ActionType.FILTER_CHANGED, payload: filters });
|
||||
}, [filters]);
|
||||
|
||||
if (error)
|
||||
|
@ -161,13 +176,13 @@ export const ReleaseTable = () => {
|
|||
return <p>Loading...</p>;
|
||||
|
||||
if (!data)
|
||||
return <EmptyListState text="No recent activity" />
|
||||
return <EmptyListState text="No recent activity" />;
|
||||
|
||||
// Render the UI for your table
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex mb-6">
|
||||
{headerGroups.map((headerGroup: { headers: any[] }) =>
|
||||
{headerGroups.map((headerGroup) =>
|
||||
headerGroup.headers.map((column) => (
|
||||
column.Filter ? (
|
||||
<div className="mt-2 sm:mt-0" key={column.id}>
|
||||
|
@ -196,7 +211,7 @@ export const ReleaseTable = () => {
|
|||
{...columnRest}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
{column.render('Header')}
|
||||
{column.render("Header")}
|
||||
{/* Add a sort direction indicator */}
|
||||
<span>
|
||||
{column.isSorted ? (
|
||||
|
@ -221,13 +236,13 @@ export const ReleaseTable = () => {
|
|||
{...getTableBodyProps()}
|
||||
className="divide-y divide-gray-200 dark:divide-gray-700"
|
||||
>
|
||||
{page.map((row: any) => {
|
||||
{page.map((row) => {
|
||||
prepareRow(row);
|
||||
|
||||
const { key: bodyRowKey, ...bodyRowRest } = row.getRowProps();
|
||||
return (
|
||||
<tr key={bodyRowKey} {...bodyRowRest}>
|
||||
{row.cells.map((cell: any) => {
|
||||
{row.cells.map((cell) => {
|
||||
const { key: cellRowKey, ...cellRowRest } = cell.getCellProps();
|
||||
return (
|
||||
<td
|
||||
|
@ -236,7 +251,7 @@ export const ReleaseTable = () => {
|
|||
role="cell"
|
||||
{...cellRowRest}
|
||||
>
|
||||
{cell.render('Cell')}
|
||||
{cell.render("Cell")}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
|
@ -263,7 +278,7 @@ export const ReleaseTable = () => {
|
|||
className="block w-full border-gray-300 rounded-md shadow-sm cursor-pointer dark:bg-gray-800 dark:border-gray-800 dark:text-gray-600 dark:hover:text-gray-500 focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||
value={pageSize}
|
||||
onChange={e => {
|
||||
setPageSize(Number(e.target.value))
|
||||
setPageSize(Number(e.target.value));
|
||||
}}
|
||||
>
|
||||
{[5, 10, 20, 50].map(pageSize => (
|
||||
|
@ -312,4 +327,4 @@ export const ReleaseTable = () => {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -4,12 +4,11 @@ import { APIClient } from "../../api/APIClient";
|
|||
import { Checkbox } from "../../components/Checkbox";
|
||||
import { SettingsContext } from "../../utils/Context";
|
||||
|
||||
|
||||
function ApplicationSettings() {
|
||||
const [settings, setSettings] = SettingsContext.use();
|
||||
|
||||
const { isLoading, data } = useQuery(
|
||||
['config'],
|
||||
["config"],
|
||||
() => APIClient.config.get(),
|
||||
{
|
||||
retry: false,
|
||||
|
@ -125,7 +124,7 @@ function ApplicationSettings() {
|
|||
</div>
|
||||
</form>
|
||||
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default ApplicationSettings;
|
|
@ -13,10 +13,10 @@ interface DLSettingsItemProps {
|
|||
}
|
||||
|
||||
function DownloadClientSettingsListItem({ client, idx }: DLSettingsItemProps) {
|
||||
const [updateClientIsOpen, toggleUpdateClient] = useToggle(false)
|
||||
const [updateClientIsOpen, toggleUpdateClient] = useToggle(false);
|
||||
|
||||
return (
|
||||
<tr key={client.name} className={idx % 2 === 0 ? 'light:bg-white' : 'light:bg-gray-50'}>
|
||||
<tr key={client.name} className={idx % 2 === 0 ? "light:bg-white" : "light:bg-gray-50"}>
|
||||
<DownloadClientUpdateForm client={client} isOpen={updateClientIsOpen} toggle={toggleUpdateClient} />
|
||||
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
|
@ -24,16 +24,16 @@ function DownloadClientSettingsListItem({ client, idx }: DLSettingsItemProps) {
|
|||
checked={client.enabled}
|
||||
onChange={toggleUpdateClient}
|
||||
className={classNames(
|
||||
client.enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600',
|
||||
'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'
|
||||
client.enabled ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-600",
|
||||
"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"
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
client.enabled ? 'translate-x-5' : 'translate-x-0',
|
||||
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
||||
client.enabled ? "translate-x-5" : "translate-x-0",
|
||||
"inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
|
@ -47,14 +47,14 @@ function DownloadClientSettingsListItem({ client, idx }: DLSettingsItemProps) {
|
|||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DownloadClientSettings() {
|
||||
const [addClientIsOpen, toggleAddClient] = useToggle(false)
|
||||
const [addClientIsOpen, toggleAddClient] = useToggle(false);
|
||||
|
||||
const { error, data } = useQuery(
|
||||
'downloadClients',
|
||||
"downloadClients",
|
||||
APIClient.download_clients.getAll,
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
|
@ -138,7 +138,7 @@ function DownloadClientSettings() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default DownloadClientSettings;
|
|
@ -3,27 +3,28 @@ import { useMutation, useQuery, useQueryClient } from "react-query";
|
|||
import { APIClient } from "../../api/APIClient";
|
||||
import { Menu, Switch, Transition } from "@headlessui/react";
|
||||
|
||||
import {classNames} from "../../utils";
|
||||
import {Fragment, useRef, useState} from "react";
|
||||
import {toast} from "react-hot-toast";
|
||||
import { classNames } from "../../utils";
|
||||
import { Fragment, useRef, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import Toast from "../../components/notifications/Toast";
|
||||
import {queryClient} from "../../App";
|
||||
import {DeleteModal} from "../../components/modals";
|
||||
import { queryClient } from "../../App";
|
||||
import { DeleteModal } from "../../components/modals";
|
||||
import {
|
||||
DotsHorizontalIcon,
|
||||
PencilAltIcon,
|
||||
SwitchHorizontalIcon,
|
||||
TrashIcon
|
||||
} from "@heroicons/react/outline";
|
||||
import {FeedUpdateForm} from "../../forms/settings/FeedForms";
|
||||
import { FeedUpdateForm } from "../../forms/settings/FeedForms";
|
||||
import { EmptySimple } from "../../components/emptystates";
|
||||
import { componentMapType } from "../../forms/settings/DownloadClientForms";
|
||||
|
||||
function FeedSettings() {
|
||||
const {data} = useQuery<Feed[], Error>('feeds', APIClient.feeds.find,
|
||||
const { data } = useQuery<Feed[], Error>("feeds", APIClient.feeds.find,
|
||||
{
|
||||
refetchOnWindowFocus: false
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||
|
@ -61,7 +62,7 @@ function FeedSettings() {
|
|||
: <EmptySimple title="No feeds" subtitle="Setup via indexers" />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const ImplementationTorznab = () => (
|
||||
|
@ -70,20 +71,20 @@ const ImplementationTorznab = () => (
|
|||
>
|
||||
Torznab
|
||||
</span>
|
||||
)
|
||||
);
|
||||
|
||||
export const ImplementationMap: any = {
|
||||
"TORZNAB": <ImplementationTorznab/>,
|
||||
export const ImplementationMap: componentMapType = {
|
||||
"TORZNAB": <ImplementationTorznab/>
|
||||
};
|
||||
|
||||
interface ListItemProps {
|
||||
feed: Feed;
|
||||
}
|
||||
|
||||
function ListItem({feed}: ListItemProps) {
|
||||
const [updateFormIsOpen, toggleUpdateForm] = useToggle(false)
|
||||
function ListItem({ feed }: ListItemProps) {
|
||||
const [updateFormIsOpen, toggleUpdateForm] = useToggle(false);
|
||||
|
||||
const [enabled, setEnabled] = useState(feed.enabled)
|
||||
const [enabled, setEnabled] = useState(feed.enabled);
|
||||
|
||||
const updateMutation = useMutation(
|
||||
(status: boolean) => APIClient.feeds.toggleEnable(feed.id, status),
|
||||
|
@ -91,7 +92,7 @@ function ListItem({feed}: ListItemProps) {
|
|||
onSuccess: () => {
|
||||
toast.custom((t) => <Toast type="success"
|
||||
body={`${feed.name} was ${enabled ? "disabled" : "enabled"} successfully`}
|
||||
t={t}/>)
|
||||
t={t}/>);
|
||||
|
||||
queryClient.invalidateQueries(["feeds"]);
|
||||
queryClient.invalidateQueries(["feeds", feed?.id]);
|
||||
|
@ -102,7 +103,7 @@ function ListItem({feed}: ListItemProps) {
|
|||
const toggleActive = (status: boolean) => {
|
||||
setEnabled(status);
|
||||
updateMutation.mutate(status);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<li key={feed.id} className="text-gray-500 dark:text-gray-400">
|
||||
|
@ -114,16 +115,16 @@ function ListItem({feed}: ListItemProps) {
|
|||
checked={feed.enabled}
|
||||
onChange={toggleActive}
|
||||
className={classNames(
|
||||
feed.enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600',
|
||||
'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'
|
||||
feed.enabled ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-600",
|
||||
"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"
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
feed.enabled ? 'translate-x-5' : 'translate-x-0',
|
||||
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
||||
feed.enabled ? "translate-x-5" : "translate-x-0",
|
||||
"inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
|
@ -143,7 +144,7 @@ function ListItem({feed}: ListItemProps) {
|
|||
</div>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
interface FeedItemDropdownProps {
|
||||
|
@ -155,8 +156,8 @@ interface FeedItemDropdownProps {
|
|||
const FeedItemDropdown = ({
|
||||
feed,
|
||||
onToggle,
|
||||
toggleUpdate,
|
||||
}: FeedItemDropdownProps) => {
|
||||
toggleUpdate
|
||||
}: FeedItemDropdownProps) => {
|
||||
const cancelModalButtonRef = useRef(null);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
@ -207,7 +208,7 @@ const FeedItemDropdown = ({
|
|||
>
|
||||
<div className="px-1 py-1">
|
||||
<Menu.Item>
|
||||
{({active}) => (
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={classNames(
|
||||
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||
|
@ -227,7 +228,7 @@ const FeedItemDropdown = ({
|
|||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({active}) => (
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={classNames(
|
||||
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||
|
@ -249,7 +250,7 @@ const FeedItemDropdown = ({
|
|||
</div>
|
||||
<div className="px-1 py-1">
|
||||
<Menu.Item>
|
||||
{({active}) => (
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={classNames(
|
||||
active ? "bg-red-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||
|
@ -273,6 +274,6 @@ const FeedItemDropdown = ({
|
|||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default FeedSettings;
|
|
@ -5,6 +5,7 @@ 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";
|
||||
|
||||
const ImplementationIRC = () => (
|
||||
<span
|
||||
|
@ -12,7 +13,7 @@ const ImplementationIRC = () => (
|
|||
>
|
||||
IRC
|
||||
</span>
|
||||
)
|
||||
);
|
||||
|
||||
const ImplementationTorznab = () => (
|
||||
<span
|
||||
|
@ -20,15 +21,19 @@ const ImplementationTorznab = () => (
|
|||
>
|
||||
Torznab
|
||||
</span>
|
||||
)
|
||||
);
|
||||
|
||||
const implementationMap: any = {
|
||||
const implementationMap: componentMapType = {
|
||||
"irc": <ImplementationIRC/>,
|
||||
"torznab": <ImplementationTorznab />,
|
||||
"torznab": <ImplementationTorznab />
|
||||
};
|
||||
|
||||
const ListItem = ({ indexer }: any) => {
|
||||
const [updateIsOpen, toggleUpdate] = useToggle(false)
|
||||
interface ListItemProps {
|
||||
indexer: IndexerDefinition;
|
||||
}
|
||||
|
||||
const ListItem = ({ indexer }: ListItemProps) => {
|
||||
const [updateIsOpen, toggleUpdate] = useToggle(false);
|
||||
|
||||
return (
|
||||
<tr key={indexer.name}>
|
||||
|
@ -36,19 +41,19 @@ const ListItem = ({ indexer }: any) => {
|
|||
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<Switch
|
||||
checked={indexer.enabled}
|
||||
checked={indexer.enabled ?? false}
|
||||
onChange={toggleUpdate}
|
||||
className={classNames(
|
||||
indexer.enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600',
|
||||
'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'
|
||||
indexer.enabled ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-600",
|
||||
"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"
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Enable</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
indexer.enabled ? 'translate-x-5' : 'translate-x-0',
|
||||
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
||||
indexer.enabled ? "translate-x-5" : "translate-x-0",
|
||||
"inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
|
@ -61,14 +66,14 @@ const ListItem = ({ indexer }: any) => {
|
|||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function IndexerSettings() {
|
||||
const [addIndexerIsOpen, toggleAddIndexer] = useToggle(false)
|
||||
const [addIndexerIsOpen, toggleAddIndexer] = useToggle(false);
|
||||
|
||||
const { error, data } = useQuery(
|
||||
'indexer',
|
||||
"indexer",
|
||||
APIClient.indexers.getAll,
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
|
@ -146,7 +151,7 @@ function IndexerSettings() {
|
|||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default IndexerSettings;
|
|
@ -13,7 +13,7 @@ import { APIClient } from "../../api/APIClient";
|
|||
import { EmptySimple } from "../../components/emptystates";
|
||||
|
||||
export const IrcSettings = () => {
|
||||
const [addNetworkIsOpen, toggleAddNetwork] = useToggle(false)
|
||||
const [addNetworkIsOpen, toggleAddNetwork] = useToggle(false);
|
||||
|
||||
const { data } = useQuery(
|
||||
"networks",
|
||||
|
@ -66,8 +66,8 @@ export const IrcSettings = () => {
|
|||
) : <EmptySimple title="No networks" subtitle="Add a new network" buttonText="New network" buttonAction={toggleAddNetwork} />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
interface ListItemProps {
|
||||
idx: number;
|
||||
|
@ -75,7 +75,7 @@ interface ListItemProps {
|
|||
}
|
||||
|
||||
const ListItem = ({ idx, network }: ListItemProps) => {
|
||||
const [updateIsOpen, toggleUpdate] = useToggle(false)
|
||||
const [updateIsOpen, toggleUpdate] = useToggle(false);
|
||||
const [edit, toggleEdit] = useToggle(false);
|
||||
|
||||
return (
|
||||
|
@ -153,5 +153,5 @@ const ListItem = ({ idx, network }: ListItemProps) => {
|
|||
</div>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,13 +7,13 @@ import { Switch } from "@headlessui/react";
|
|||
import { classNames } from "../../utils";
|
||||
|
||||
function NotificationSettings() {
|
||||
const [addNotificationsIsOpen, toggleAddNotifications] = useToggle(false)
|
||||
const [addNotificationsIsOpen, toggleAddNotifications] = useToggle(false);
|
||||
|
||||
const { data } = useQuery<Notification[], Error>('notifications', APIClient.notifications.getAll,
|
||||
const { data } = useQuery<Notification[], Error>("notifications", APIClient.notifications.getAll,
|
||||
{
|
||||
refetchOnWindowFocus: false
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||
|
@ -56,7 +56,7 @@ function NotificationSettings() {
|
|||
: <EmptySimple title="No notifications setup" subtitle="Add a new notification" buttonText="New notification" buttonAction={toggleAddNotifications} />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
interface ListItemProps {
|
||||
|
@ -64,7 +64,7 @@ interface ListItemProps {
|
|||
}
|
||||
|
||||
function ListItem({ notification }: ListItemProps) {
|
||||
const [updateFormIsOpen, toggleUpdateForm] = useToggle(false)
|
||||
const [updateFormIsOpen, toggleUpdateForm] = useToggle(false);
|
||||
|
||||
return (
|
||||
<li key={notification.id} className="text-gray-500 dark:text-gray-400">
|
||||
|
@ -76,16 +76,16 @@ function ListItem({ notification }: ListItemProps) {
|
|||
checked={notification.enabled}
|
||||
onChange={toggleUpdateForm}
|
||||
className={classNames(
|
||||
notification.enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600',
|
||||
'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'
|
||||
notification.enabled ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-600",
|
||||
"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"
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
notification.enabled ? 'translate-x-5' : 'translate-x-0',
|
||||
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
||||
notification.enabled ? "translate-x-5" : "translate-x-0",
|
||||
"inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
|
@ -113,7 +113,7 @@ function ListItem({ notification }: ListItemProps) {
|
|||
</div>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default NotificationSettings;
|
|
@ -15,7 +15,6 @@ export const RegexPlayground = () => {
|
|||
const matches = line.matchAll(regexp);
|
||||
|
||||
let lastIndex = 0;
|
||||
// @ts-ignore
|
||||
for (const match of matches) {
|
||||
if (match.index === undefined)
|
||||
continue;
|
||||
|
@ -52,7 +51,7 @@ export const RegexPlayground = () => {
|
|||
});
|
||||
|
||||
setOutput(results);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9">
|
||||
|
@ -103,4 +102,4 @@ export const RegexPlayground = () => {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -13,19 +13,19 @@ function ReleaseSettings() {
|
|||
const deleteMutation = useMutation(() => APIClient.release.delete(), {
|
||||
onSuccess: () => {
|
||||
toast.custom((t) => (
|
||||
<Toast type="success" body={`All releases was deleted`} t={t}/>
|
||||
<Toast type="success" body={"All releases was deleted"} t={t}/>
|
||||
));
|
||||
|
||||
// Invalidate filters just in case, most likely not necessary but can't hurt.
|
||||
queryClient.invalidateQueries("releases");
|
||||
|
||||
toggleDeleteModal()
|
||||
toggleDeleteModal();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const deleteAction = () => {
|
||||
deleteMutation.mutate()
|
||||
}
|
||||
deleteMutation.mutate();
|
||||
};
|
||||
|
||||
const cancelModalButtonRef = useRef(null);
|
||||
|
||||
|
@ -36,7 +36,7 @@ function ReleaseSettings() {
|
|||
toggle={toggleDeleteModal}
|
||||
buttonRef={cancelModalButtonRef}
|
||||
deleteAction={deleteAction}
|
||||
title={`Delete all releases`}
|
||||
title={"Delete all releases"}
|
||||
text="Are you sure you want to delete all releases? This action cannot be undone."
|
||||
/>
|
||||
|
||||
|
@ -75,7 +75,7 @@ function ReleaseSettings() {
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default ReleaseSettings;
|
|
@ -1,10 +1,10 @@
|
|||
const { createProxyMiddleware } = require('http-proxy-middleware');
|
||||
const { createProxyMiddleware } = require("http-proxy-middleware");
|
||||
|
||||
module.exports = function(app) {
|
||||
app.use(
|
||||
'/api',
|
||||
"/api",
|
||||
createProxyMiddleware({
|
||||
target: 'http://127.0.0.1:7474',
|
||||
target: "http://127.0.0.1:7474",
|
||||
changeOrigin: true,
|
||||
})
|
||||
);
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
import "@testing-library/jest-dom";
|
||||
|
|
25
web/src/types/Download.d.ts
vendored
25
web/src/types/Download.d.ts
vendored
|
@ -1,11 +1,21 @@
|
|||
type DownloadClientType =
|
||||
'QBITTORRENT' |
|
||||
'DELUGE_V1' |
|
||||
'DELUGE_V2' |
|
||||
'RADARR' |
|
||||
'SONARR' |
|
||||
'LIDARR' |
|
||||
'WHISPARR';
|
||||
"QBITTORRENT" |
|
||||
"DELUGE_V1" |
|
||||
"DELUGE_V2" |
|
||||
"RADARR" |
|
||||
"SONARR" |
|
||||
"LIDARR" |
|
||||
"WHISPARR";
|
||||
|
||||
// export enum DownloadClientTypeEnum {
|
||||
// QBITTORRENT = "QBITTORRENT",
|
||||
// DELUGE_V1 = "DELUGE_V1",
|
||||
// DELUGE_V2 = "DELUGE_V2",
|
||||
// RADARR = "RADARR",
|
||||
// SONARR = "SONARR",
|
||||
// LIDARR = "LIDARR",
|
||||
// WHISPARR = "WHISPARR"
|
||||
// }
|
||||
|
||||
interface DownloadClientRules {
|
||||
enabled: boolean;
|
||||
|
@ -27,7 +37,6 @@ interface DownloadClientSettings {
|
|||
}
|
||||
|
||||
interface DownloadClient {
|
||||
id?: number;
|
||||
id: number;
|
||||
name: string;
|
||||
type: DownloadClientType;
|
||||
|
|
2
web/src/types/Filter.d.ts
vendored
2
web/src/types/Filter.d.ts
vendored
|
@ -83,4 +83,4 @@ interface Action {
|
|||
client_id?: number;
|
||||
}
|
||||
|
||||
type ActionType = 'TEST' | 'EXEC' | 'WATCH_FOLDER' | 'WEBHOOK' | DownloadClientType;
|
||||
type ActionType = "TEST" | "EXEC" | "WATCH_FOLDER" | "WEBHOOK" | DownloadClientType;
|
||||
|
|
2
web/src/types/Indexer.d.ts
vendored
2
web/src/types/Indexer.d.ts
vendored
|
@ -8,7 +8,7 @@ interface Indexer {
|
|||
}
|
||||
|
||||
interface IndexerDefinition {
|
||||
id?: number;
|
||||
id: number;
|
||||
name: string;
|
||||
identifier: string;
|
||||
implementation: string;
|
||||
|
|
2
web/src/types/Irc.d.ts
vendored
2
web/src/types/Irc.d.ts
vendored
|
@ -10,7 +10,7 @@ interface IrcNetwork {
|
|||
nickserv?: NickServ; // optional
|
||||
channels: IrcChannel[];
|
||||
connected: boolean;
|
||||
connected_since: Time;
|
||||
connected_since: string;
|
||||
}
|
||||
|
||||
interface IrcNetworkCreate {
|
||||
|
|
2
web/src/types/Notification.d.ts
vendored
2
web/src/types/Notification.d.ts
vendored
|
@ -1,4 +1,4 @@
|
|||
type NotificationType = 'DISCORD';
|
||||
type NotificationType = "DISCORD";
|
||||
|
||||
interface Notification {
|
||||
id: number;
|
||||
|
|
|
@ -19,7 +19,7 @@ export const InitializeGlobalContext = () => {
|
|||
)
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
interface AuthInfo {
|
||||
username: string;
|
||||
|
|
|
@ -7,32 +7,32 @@ export function sleep(ms: number) {
|
|||
|
||||
// get baseUrl sent from server rendered index template
|
||||
export function baseUrl() {
|
||||
let baseUrl = ""
|
||||
let baseUrl = "";
|
||||
if (window.APP.baseUrl) {
|
||||
if (window.APP.baseUrl === "/") {
|
||||
baseUrl = "/"
|
||||
baseUrl = "/";
|
||||
} else if (window.APP.baseUrl === "{{.BaseUrl}}") {
|
||||
baseUrl = "/"
|
||||
baseUrl = "/";
|
||||
} else if (window.APP.baseUrl === "/autobrr/") {
|
||||
baseUrl = "/autobrr/"
|
||||
baseUrl = "/autobrr/";
|
||||
} else {
|
||||
baseUrl = window.APP.baseUrl
|
||||
baseUrl = window.APP.baseUrl;
|
||||
}
|
||||
}
|
||||
|
||||
return baseUrl
|
||||
return baseUrl;
|
||||
}
|
||||
|
||||
// get sseBaseUrl for SSE
|
||||
export function sseBaseUrl() {
|
||||
if (process.env.NODE_ENV === "development")
|
||||
return `http://localhost:7474/`;
|
||||
return "http://localhost:7474/";
|
||||
|
||||
return `${window.location.origin}${baseUrl()}`;
|
||||
}
|
||||
|
||||
export function classNames(...classes: string[]) {
|
||||
return classes.filter(Boolean).join(' ')
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
// column widths for inputs etc
|
||||
|
@ -41,9 +41,9 @@ export type COL_WIDTHS = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
|||
// simplify date
|
||||
export function simplifyDate(date: string) {
|
||||
if (date !== "0001-01-01T00:00:00Z") {
|
||||
return formatISO9075(new Date(date))
|
||||
return formatISO9075(new Date(date));
|
||||
}
|
||||
return "n/a"
|
||||
return "n/a";
|
||||
}
|
||||
|
||||
// if empty date show as n/a
|
||||
|
@ -52,16 +52,16 @@ export function IsEmptyDate(date: string) {
|
|||
return formatDistanceToNowStrict(
|
||||
new Date(date),
|
||||
{ addSuffix: true }
|
||||
)
|
||||
);
|
||||
}
|
||||
return "n/a"
|
||||
return "n/a";
|
||||
}
|
||||
|
||||
export function slugify(str: string) {
|
||||
return str
|
||||
.normalize('NFKD')
|
||||
.normalize("NFKD")
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.replace(/[^\w\s-]/g, "")
|
||||
.trim()
|
||||
.replace(/[-\s]+/g, '-');
|
||||
.replace(/[-\s]+/g, "-");
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
],
|
||||
"types": [],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"skipLibCheck": false,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
|
|
|
@ -2088,7 +2088,22 @@
|
|||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^5.10.2", "@typescript-eslint/eslint-plugin@^5.5.0":
|
||||
"@typescript-eslint/eslint-plugin@^5.18.0":
|
||||
version "5.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.23.0.tgz#bc4cbcf91fbbcc2e47e534774781b82ae25cc3d8"
|
||||
integrity sha512-hEcSmG4XodSLiAp1uxv/OQSGsDY6QN3TcRU32gANp+19wGE1QQZLRS8/GV58VRUoXhnkuJ3ZxNQ3T6Z6zM59DA==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "5.23.0"
|
||||
"@typescript-eslint/type-utils" "5.23.0"
|
||||
"@typescript-eslint/utils" "5.23.0"
|
||||
debug "^4.3.2"
|
||||
functional-red-black-tree "^1.0.1"
|
||||
ignore "^5.1.8"
|
||||
regexpp "^3.2.0"
|
||||
semver "^7.3.5"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^5.5.0":
|
||||
version "5.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.18.0.tgz#950df411cec65f90d75d6320a03b2c98f6c3af7d"
|
||||
integrity sha512-tzrmdGMJI/uii9/V6lurMo4/o+dMTKDH82LkNjhJ3adCW22YQydoRs5MwTiqxGF9CSYxPxQ7EYb4jLNlIs+E+A==
|
||||
|
@ -2110,7 +2125,17 @@
|
|||
dependencies:
|
||||
"@typescript-eslint/utils" "5.18.0"
|
||||
|
||||
"@typescript-eslint/parser@^5.10.2", "@typescript-eslint/parser@^5.5.0":
|
||||
"@typescript-eslint/parser@^5.18.0":
|
||||
version "5.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.23.0.tgz#443778e1afc9a8ff180f91b5e260ac3bec5e2de1"
|
||||
integrity sha512-V06cYUkqcGqpFjb8ttVgzNF53tgbB/KoQT/iB++DOIExKmzI9vBJKjZKt/6FuV9c+zrDsvJKbJ2DOCYwX91cbw==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "5.23.0"
|
||||
"@typescript-eslint/types" "5.23.0"
|
||||
"@typescript-eslint/typescript-estree" "5.23.0"
|
||||
debug "^4.3.2"
|
||||
|
||||
"@typescript-eslint/parser@^5.5.0":
|
||||
version "5.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.18.0.tgz#2bcd4ff21df33621df33e942ccb21cb897f004c6"
|
||||
integrity sha512-+08nYfurBzSSPndngnHvFw/fniWYJ5ymOrn/63oMIbgomVQOvIDhBoJmYZ9lwQOCnQV9xHGvf88ze3jFGUYooQ==
|
||||
|
@ -2128,6 +2153,14 @@
|
|||
"@typescript-eslint/types" "5.18.0"
|
||||
"@typescript-eslint/visitor-keys" "5.18.0"
|
||||
|
||||
"@typescript-eslint/scope-manager@5.23.0":
|
||||
version "5.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.23.0.tgz#4305e61c2c8e3cfa3787d30f54e79430cc17ce1b"
|
||||
integrity sha512-EhjaFELQHCRb5wTwlGsNMvzK9b8Oco4aYNleeDlNuL6qXWDF47ch4EhVNPh8Rdhf9tmqbN4sWDk/8g+Z/J8JVw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.23.0"
|
||||
"@typescript-eslint/visitor-keys" "5.23.0"
|
||||
|
||||
"@typescript-eslint/type-utils@5.18.0":
|
||||
version "5.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.18.0.tgz#62dbfc8478abf36ba94a90ddf10be3cc8e471c74"
|
||||
|
@ -2137,11 +2170,25 @@
|
|||
debug "^4.3.2"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/type-utils@5.23.0":
|
||||
version "5.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.23.0.tgz#f852252f2fc27620d5bb279d8fed2a13d2e3685e"
|
||||
integrity sha512-iuI05JsJl/SUnOTXA9f4oI+/4qS/Zcgk+s2ir+lRmXI+80D8GaGwoUqs4p+X+4AxDolPpEpVUdlEH4ADxFy4gw==
|
||||
dependencies:
|
||||
"@typescript-eslint/utils" "5.23.0"
|
||||
debug "^4.3.2"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/types@5.18.0":
|
||||
version "5.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.18.0.tgz#4f0425d85fdb863071680983853c59a62ce9566e"
|
||||
integrity sha512-bhV1+XjM+9bHMTmXi46p1Led5NP6iqQcsOxgx7fvk6gGiV48c6IynY0apQb7693twJDsXiVzNXTflhplmaiJaw==
|
||||
|
||||
"@typescript-eslint/types@5.23.0":
|
||||
version "5.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.23.0.tgz#8733de0f58ae0ed318dbdd8f09868cdbf9f9ad09"
|
||||
integrity sha512-NfBsV/h4dir/8mJwdZz7JFibaKC3E/QdeMEDJhiAE3/eMkoniZ7MjbEMCGXw6MZnZDMN3G9S0mH/6WUIj91dmw==
|
||||
|
||||
"@typescript-eslint/typescript-estree@5.18.0":
|
||||
version "5.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.18.0.tgz#6498e5ee69a32e82b6e18689e2f72e4060986474"
|
||||
|
@ -2155,6 +2202,19 @@
|
|||
semver "^7.3.5"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/typescript-estree@5.23.0":
|
||||
version "5.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.23.0.tgz#dca5f10a0a85226db0796e8ad86addc9aee52065"
|
||||
integrity sha512-xE9e0lrHhI647SlGMl+m+3E3CKPF1wzvvOEWnuE3CCjjT7UiRnDGJxmAcVKJIlFgK6DY9RB98eLr1OPigPEOGg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.23.0"
|
||||
"@typescript-eslint/visitor-keys" "5.23.0"
|
||||
debug "^4.3.2"
|
||||
globby "^11.0.4"
|
||||
is-glob "^4.0.3"
|
||||
semver "^7.3.5"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/utils@5.18.0", "@typescript-eslint/utils@^5.13.0":
|
||||
version "5.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.18.0.tgz#27fc84cf95c1a96def0aae31684cb43a37e76855"
|
||||
|
@ -2167,6 +2227,18 @@
|
|||
eslint-scope "^5.1.1"
|
||||
eslint-utils "^3.0.0"
|
||||
|
||||
"@typescript-eslint/utils@5.23.0":
|
||||
version "5.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.23.0.tgz#4691c3d1b414da2c53d8943310df36ab1c50648a"
|
||||
integrity sha512-dbgaKN21drqpkbbedGMNPCtRPZo1IOUr5EI9Jrrh99r5UW5Q0dz46RKXeSBoPV+56R6dFKpbrdhgUNSJsDDRZA==
|
||||
dependencies:
|
||||
"@types/json-schema" "^7.0.9"
|
||||
"@typescript-eslint/scope-manager" "5.23.0"
|
||||
"@typescript-eslint/types" "5.23.0"
|
||||
"@typescript-eslint/typescript-estree" "5.23.0"
|
||||
eslint-scope "^5.1.1"
|
||||
eslint-utils "^3.0.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@5.18.0":
|
||||
version "5.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.18.0.tgz#c7c07709823804171d569017f3b031ced7253e60"
|
||||
|
@ -2175,6 +2247,14 @@
|
|||
"@typescript-eslint/types" "5.18.0"
|
||||
eslint-visitor-keys "^3.0.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@5.23.0":
|
||||
version "5.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.23.0.tgz#057c60a7ca64667a39f991473059377a8067c87b"
|
||||
integrity sha512-Vd4mFNchU62sJB8pX19ZSPog05B0Y0CE2UxAZPT5k4iqhRYjPnqyY3woMxCd0++t9OTqkgjST+1ydLBi7e2Fvg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.23.0"
|
||||
eslint-visitor-keys "^3.0.0"
|
||||
|
||||
"@webassemblyjs/ast@1.11.1":
|
||||
version "1.11.1"
|
||||
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue