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