mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +00:00
enhancement(web): ui overhaul (#1155)
* Various WebUI changes and fixes. * feat(tooltip): make tooltip display upwards * fix(tooltip): place tooltip to the right * fix(web): add missing ml-px to SwitchGroup header current: https://i.imgur.com/2WXstPV.png new: https://i.imgur.com/QGQ49mP.png * fix(web): collapse sections * fix(web): improve freeleech section * fix(web): rename action to action_components Renamed the 'action' folder to 'action_components' to resolve import issues due to case sensitivity. * fix(web): align CollapsibleSection Old Advanced tab: https://i.imgur.com/MXaJ5eJ.png New Advanced tab: https://i.imgur.com/4nPJJRw.png Music tab for comparison: https://i.imgur.com/I59X7ot.png * fix(web): remove invalid CSS class * revert: vertical padding on switchgroup added py-0 on the freeleech part instead * feat(settings): add back log files * fix(settings): irc channels and font sizes * fix(components): radio select roundness * fix(styling): various minor changes * fix(filters): remove jitter fields --------- Co-authored-by: ze0s <43699394+zze0s@users.noreply.github.com> Co-authored-by: soup <soup@r4tio.dev> Co-authored-by: ze0s <ze0s@riseup.net>
This commit is contained in:
parent
a274d9ddce
commit
e842a7bd42
84 changed files with 4378 additions and 4361 deletions
|
@ -81,14 +81,14 @@ export function FilterAddForm({ isOpen, toggle }: filterAddFormProps) {
|
|||
validate={validate}
|
||||
>
|
||||
{({ 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-auto">
|
||||
<div className="flex-1">
|
||||
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
|
||||
<div className="flex items-start justify-between space-x-3">
|
||||
<div className="space-y-1">
|
||||
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Create filter</Dialog.Title>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Add new filter.
|
||||
Add new filter.
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-7 flex items-center">
|
||||
|
@ -114,6 +114,7 @@ export function FilterAddForm({ isOpen, toggle }: filterAddFormProps) {
|
|||
className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2"
|
||||
>
|
||||
Name
|
||||
<span className="text-red-500"> *</span>
|
||||
</label>
|
||||
</div>
|
||||
<Field name="name">
|
||||
|
@ -126,7 +127,7 @@ export function FilterAddForm({ isOpen, toggle }: filterAddFormProps) {
|
|||
{...field}
|
||||
id="name"
|
||||
type="text"
|
||||
className="block w-full shadow-sm dark:bg-gray-800 border-gray-300 dark:border-gray-700 sm:text-sm dark:text-white focus:ring-blue-500 dark:focus:ring-blue-500 focus:border-blue-500 dark:focus:border-blue-500 rounded-md"
|
||||
className="block w-full shadow-sm sm:text-sm rounded-md border py-2.5 focus:ring-blue-500 dark:focus:ring-blue-500 focus:border-blue-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700 bg-gray-100 dark:bg-gray-815 dark:text-gray-100"
|
||||
/>
|
||||
|
||||
{meta.touched && meta.error &&
|
||||
|
|
|
@ -70,7 +70,7 @@ export function APIKeyAddForm({ isOpen, toggle }: apiKeyAddFormProps) {
|
|||
validate={validate}
|
||||
>
|
||||
{({ 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-auto">
|
||||
<div className="flex-1">
|
||||
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
|
||||
<div className="flex items-start justify-between space-x-3">
|
||||
|
@ -116,7 +116,7 @@ export function APIKeyAddForm({ isOpen, toggle }: apiKeyAddFormProps) {
|
|||
{...field}
|
||||
id="name"
|
||||
type="text"
|
||||
className="block w-full shadow-sm dark:bg-gray-800 border-gray-300 dark:border-gray-700 sm:text-sm dark:text-white focus:ring-blue-500 dark:focus:ring-blue-500 focus:border-blue-500 dark:focus:border-blue-500 rounded-md"
|
||||
className="block w-full shadow-sm sm:text-sm focus:ring-blue-500 dark:focus:ring-blue-500 focus:border-blue-500 dark:focus:border-blue-500 rounded-md border-gray-300 dark:border-gray-700 bg-gray-100 dark:bg-gray-815 dark:text-gray-100"
|
||||
/>
|
||||
{meta.touched && meta.error && <span className="block mt-2 text-red-500">{meta.error}</span>}
|
||||
</div>
|
||||
|
|
|
@ -13,7 +13,7 @@ import { toast } from "react-hot-toast";
|
|||
import { classNames, sleep } from "@utils";
|
||||
import DEBUG from "@components/debug";
|
||||
import { APIClient } from "@api/APIClient";
|
||||
import {DownloadClientTypeOptions, DownloadRuleConditionOptions} from "@domain/constants";
|
||||
import { DownloadClientTypeOptions, DownloadRuleConditionOptions } from "@domain/constants";
|
||||
import Toast from "@components/notifications/Toast";
|
||||
import { useToggle } from "@hooks/hooks";
|
||||
import { DeleteModal } from "@components/modals";
|
||||
|
@ -26,7 +26,7 @@ import {
|
|||
} from "@components/inputs";
|
||||
import { clientKeys } from "@screens/settings/DownloadClient";
|
||||
import { DocsLink, ExternalLink } from "@components/ExternalLink";
|
||||
import {SelectFieldBasic} from "@components/inputs/select_wide";
|
||||
import { SelectFieldBasic } from "@components/inputs/select_wide";
|
||||
|
||||
interface InitialValuesSettings {
|
||||
basic?: {
|
||||
|
@ -474,11 +474,11 @@ function FormFieldsRulesQbit() {
|
|||
{settings.rules?.ignore_slow_torrents === true && (
|
||||
<>
|
||||
<SelectFieldBasic
|
||||
name="settings.rules.ignore_slow_torrents_condition"
|
||||
label="Ignore condition"
|
||||
placeholder="Select ignore condition"
|
||||
options={DownloadRuleConditionOptions}
|
||||
tooltip={<p>Choose whether to respect or ignore the <code className="text-blue-400">Max active downloads</code> setting before checking speed thresholds.</p>}
|
||||
name="settings.rules.ignore_slow_torrents_condition"
|
||||
label="Ignore condition"
|
||||
placeholder="Select ignore condition"
|
||||
options={DownloadRuleConditionOptions}
|
||||
tooltip={<p>Choose whether to respect or ignore the <code className="text-blue-400">Max active downloads</code> setting before checking speed thresholds.</p>}
|
||||
/>
|
||||
<NumberFieldWide
|
||||
name="settings.rules.download_speed_threshold"
|
||||
|
@ -750,7 +750,7 @@ export function DownloadClientAddForm({ isOpen, toggle }: formProps) {
|
|||
>
|
||||
{({ handleSubmit, values }) => (
|
||||
<Form
|
||||
className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll"
|
||||
className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-auto"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<div className="flex-1">
|
||||
|
@ -945,7 +945,7 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: updateFormP
|
|||
{({ handleSubmit, values }) => {
|
||||
return (
|
||||
<Form
|
||||
className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll"
|
||||
className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-auto"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<div className="flex-1">
|
||||
|
@ -962,7 +962,7 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: updateFormP
|
|||
<div className="h-7 flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="bg-white dark:bg-gray-800 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-500"
|
||||
onClick={toggle}
|
||||
>
|
||||
<span className="sr-only">Close panel</span>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import { Fragment, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select";
|
||||
import Select from "react-select";
|
||||
import type { FieldProps } from "formik";
|
||||
import { Field, Form, Formik, FormikValues } from "formik";
|
||||
import { XMarkIcon } from "@heroicons/react/24/solid";
|
||||
|
@ -15,47 +15,15 @@ import { Dialog, Transition } from "@headlessui/react";
|
|||
import { classNames, sleep } from "@utils";
|
||||
import DEBUG from "@components/debug";
|
||||
import { APIClient } from "@api/APIClient";
|
||||
import { PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "@components/inputs";
|
||||
import { SlideOver } from "@components/panels";
|
||||
import Toast from "@components/notifications/Toast";
|
||||
import { PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "@components/inputs";
|
||||
import { SelectFieldBasic, SelectFieldCreatable } from "@components/inputs/select_wide";
|
||||
import { FeedDownloadTypeOptions } from "@domain/constants";
|
||||
import { feedKeys } from "@screens/settings/Feed";
|
||||
import { indexerKeys } from "@screens/settings/Indexer";
|
||||
import { DocsLink } from "@components/ExternalLink";
|
||||
|
||||
const Input = (props: InputProps) => (
|
||||
<components.Input
|
||||
{...props}
|
||||
inputClassName="outline-none border-none shadow-none focus:ring-transparent"
|
||||
className="text-gray-400 dark:text-gray-100"
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
|
||||
const Control = (props: ControlProps) => (
|
||||
<components.Control
|
||||
{...props}
|
||||
className="p-1 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
|
||||
const Menu = (props: MenuProps) => (
|
||||
<components.Menu
|
||||
{...props}
|
||||
className="dark:bg-gray-800 border border-gray-300 dark:border-gray-700 dark:text-gray-400 rounded-md shadow-sm cursor-pointer"
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
|
||||
const Option = (props: OptionProps) => (
|
||||
<components.Option
|
||||
{...props}
|
||||
className="dark:text-gray-400 dark:bg-gray-800 dark:hover:bg-gray-900 dark:focus:bg-gray-900 cursor-pointer"
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
import * as common from "@components/inputs/common";
|
||||
|
||||
// const isRequired = (message: string) => (value?: string | undefined) => (!!value ? undefined : message);
|
||||
|
||||
|
@ -73,51 +41,54 @@ function validateField(s: IndexerSetting) {
|
|||
}
|
||||
|
||||
const IrcSettingFields = (ind: IndexerDefinition, indexer: string) => {
|
||||
if (indexer !== "") {
|
||||
return (
|
||||
<Fragment>
|
||||
{ind && ind.irc && ind.irc.settings && (
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
|
||||
<div className="px-4 space-y-1">
|
||||
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">IRC</Dialog.Title>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-200">
|
||||
Networks and channels are configured automatically in the background.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{ind.irc.settings.map((f: IndexerSetting, idx: number) => {
|
||||
switch (f.type) {
|
||||
case "text":
|
||||
return (
|
||||
<TextFieldWide
|
||||
key={idx}
|
||||
name={`irc.${f.name}`}
|
||||
label={f.label}
|
||||
required={f.required}
|
||||
help={f.help}
|
||||
autoComplete="off"
|
||||
validate={validateField(f)}
|
||||
tooltip={
|
||||
<div>
|
||||
<p>Please read our IRC guide if you are unfamiliar with IRC.</p>
|
||||
<DocsLink href="https://autobrr.com/configuration/irc" />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
case "secret":
|
||||
if (f.name === "invite_command") {
|
||||
return <PasswordFieldWide defaultVisible name={`irc.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultValue={f.default} validate={validateField(f)} />;
|
||||
}
|
||||
return <PasswordFieldWide name={`irc.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultValue={f.default} validate={validateField(f)} />;
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
if (!indexer.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{ind && ind.irc && ind.irc.settings && (
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
|
||||
<div className="px-4 space-y-1">
|
||||
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">IRC</Dialog.Title>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-200">
|
||||
Networks and channels are configured automatically in the background.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{ind.irc.settings.map((f: IndexerSetting, idx: number) => {
|
||||
switch (f.type) {
|
||||
case "text":
|
||||
return (
|
||||
<TextFieldWide
|
||||
key={idx}
|
||||
name={`irc.${f.name}`}
|
||||
label={f.label}
|
||||
required={f.required}
|
||||
help={f.help}
|
||||
autoComplete="off"
|
||||
validate={validateField(f)}
|
||||
tooltip={
|
||||
<div>
|
||||
<p>Please read our IRC guide if you are unfamiliar with IRC.</p>
|
||||
<DocsLink href="https://autobrr.com/configuration/irc" />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
case "secret":
|
||||
if (f.name === "invite_command") {
|
||||
return <PasswordFieldWide defaultVisible name={`irc.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultValue={f.default} validate={validateField(f)} />;
|
||||
}
|
||||
return <PasswordFieldWide name={`irc.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultValue={f.default} validate={validateField(f)} />;
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
const TorznabFeedSettingFields = (ind: IndexerDefinition, indexer: string) => {
|
||||
|
@ -137,10 +108,10 @@ const TorznabFeedSettingFields = (ind: IndexerDefinition, indexer: string) => {
|
|||
|
||||
{ind.torznab.settings.map((f: IndexerSetting, idx: number) => {
|
||||
switch (f.type) {
|
||||
case "text":
|
||||
return <TextFieldWide name={`feed.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} autoComplete="off" validate={validateField(f)} />;
|
||||
case "secret":
|
||||
return <PasswordFieldWide name={`feed.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultValue={f.default} validate={validateField(f)} />;
|
||||
case "text":
|
||||
return <TextFieldWide name={`feed.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} autoComplete="off" validate={validateField(f)} />;
|
||||
case "secret":
|
||||
return <PasswordFieldWide name={`feed.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultValue={f.default} validate={validateField(f)} />;
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
|
@ -176,10 +147,10 @@ const NewznabFeedSettingFields = (ind: IndexerDefinition, indexer: string) => {
|
|||
|
||||
{ind.newznab.settings.map((f: IndexerSetting, idx: number) => {
|
||||
switch (f.type) {
|
||||
case "text":
|
||||
return <TextFieldWide name={`feed.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} autoComplete="off" validate={validateField(f)} />;
|
||||
case "secret":
|
||||
return <PasswordFieldWide name={`feed.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultValue={f.default} validate={validateField(f)} />;
|
||||
case "text":
|
||||
return <TextFieldWide name={`feed.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} autoComplete="off" validate={validateField(f)} />;
|
||||
case "secret":
|
||||
return <PasswordFieldWide name={`feed.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultValue={f.default} validate={validateField(f)} />;
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
|
@ -207,10 +178,10 @@ const RSSFeedSettingFields = (ind: IndexerDefinition, indexer: string) => {
|
|||
|
||||
{ind.rss.settings.map((f: IndexerSetting, idx: number) => {
|
||||
switch (f.type) {
|
||||
case "text":
|
||||
return <TextFieldWide name={`feed.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} autoComplete="off" validate={validateField(f)} />;
|
||||
case "secret":
|
||||
return <PasswordFieldWide name={`feed.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultValue={f.default} validate={validateField(f)} />;
|
||||
case "text":
|
||||
return <TextFieldWide name={`feed.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} autoComplete="off" validate={validateField(f)} />;
|
||||
case "secret":
|
||||
return <PasswordFieldWide name={`feed.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultValue={f.default} validate={validateField(f)} />;
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
|
@ -235,28 +206,28 @@ const SettingFields = (ind: IndexerDefinition, indexer: string) => {
|
|||
<div key="opt">
|
||||
{ind && ind.settings && ind.settings.map((f, idx: number) => {
|
||||
switch (f.type) {
|
||||
case "text":
|
||||
return (
|
||||
<TextFieldWide name={`settings.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} autoComplete="off" validate={validateField(f)} />
|
||||
);
|
||||
case "secret":
|
||||
return (
|
||||
<PasswordFieldWide
|
||||
name={`settings.${f.name}`}
|
||||
label={f.label}
|
||||
required={f.required}
|
||||
key={idx}
|
||||
help={f.help}
|
||||
validate={validateField(f)}
|
||||
tooltip={
|
||||
<div>
|
||||
<p>This field does not take a full URL. Only use alphanumeric strings like <code>uqcdi67cibkx3an8cmdm</code>.</p>
|
||||
<br />
|
||||
<DocsLink href="https://autobrr.com/faqs#common-action-rejections" />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
case "text":
|
||||
return (
|
||||
<TextFieldWide name={`settings.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} autoComplete="off" validate={validateField(f)} />
|
||||
);
|
||||
case "secret":
|
||||
return (
|
||||
<PasswordFieldWide
|
||||
name={`settings.${f.name}`}
|
||||
label={f.label}
|
||||
required={f.required}
|
||||
key={idx}
|
||||
help={f.help}
|
||||
validate={validateField(f)}
|
||||
tooltip={
|
||||
<div>
|
||||
<p>This field does not take a full URL. Only use alphanumeric strings like <code>uqcdi67cibkx3an8cmdm</code>.</p>
|
||||
<br />
|
||||
<DocsLink href="https://autobrr.com/faqs#common-action-rejections" />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
|
@ -468,7 +439,7 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
|
|||
onSubmit={onSubmit}
|
||||
>
|
||||
{({ 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-auto">
|
||||
<div className="flex-1">
|
||||
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
|
||||
<div className="flex items-start justify-between space-x-3">
|
||||
|
@ -509,7 +480,14 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
|
|||
<Select {...field}
|
||||
isClearable={true}
|
||||
isSearchable={true}
|
||||
components={{ Input, Control, Menu, Option }}
|
||||
components={{
|
||||
Input: common.SelectInput,
|
||||
Control: common.SelectControl,
|
||||
Menu: common.SelectMenu,
|
||||
Option: common.SelectOption,
|
||||
IndicatorSeparator: common.IndicatorSeparator,
|
||||
DropdownIndicator: common.DropdownIndicator
|
||||
}}
|
||||
placeholder="Choose an indexer"
|
||||
styles={{
|
||||
singleValue: (base) => ({
|
||||
|
@ -566,7 +544,7 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
|
|||
name="base_url"
|
||||
label="Base URL"
|
||||
help="Override baseurl if it's blocked by your ISP."
|
||||
options={indexer.urls.map(u => ({ value: u, label: u, key: u })) }
|
||||
options={indexer.urls.map(u => ({ value: u, label: u, key: u }))}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
@ -736,9 +714,9 @@ interface IndexerUpdateInitialValues {
|
|||
}
|
||||
|
||||
interface UpdateProps {
|
||||
isOpen: boolean;
|
||||
toggle: () => void;
|
||||
indexer: IndexerDefinition;
|
||||
isOpen: boolean;
|
||||
toggle: () => void;
|
||||
indexer: IndexerDefinition;
|
||||
}
|
||||
|
||||
export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) {
|
||||
|
@ -852,7 +830,7 @@ export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) {
|
|||
<input
|
||||
type="text"
|
||||
{...field}
|
||||
className="block w-full shadow-sm dark:bg-gray-800 sm:text-sm dark:text-white focus:ring-blue-500 focus:border-blue-500 border-gray-300 dark:border-gray-700 rounded-md"
|
||||
className="block w-full shadow-sm sm:text-sm focus:ring-blue-500 focus:border-blue-500 border-gray-300 dark:border-gray-700 bg-gray-100 dark:bg-gray-815 dark:text-gray-100 rounded-md"
|
||||
/>
|
||||
{meta.touched && meta.error && <span>{meta.error}</span>}
|
||||
</div>
|
||||
|
@ -866,7 +844,7 @@ export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) {
|
|||
name="base_url"
|
||||
label="Base URL"
|
||||
help="Override baseurl if it's blocked by your ISP."
|
||||
options={indexer.urls.map(u => ({ value: u, label: u, key: u })) }
|
||||
options={indexer.urls.map(u => ({ value: u, label: u, key: u }))}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
@ -10,56 +10,67 @@ import type { FieldProps } from "formik";
|
|||
import type { FieldArrayRenderProps } from "formik";
|
||||
import { Field, FieldArray, FormikErrors, FormikValues } from "formik";
|
||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||
import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select";
|
||||
import Select from "react-select";
|
||||
import { Dialog } from "@headlessui/react";
|
||||
|
||||
import { IrcAuthMechanismTypeOptions, OptionBasicTyped } from "@domain/constants";
|
||||
import { ircKeys } from "@screens/settings/Irc";
|
||||
import { APIClient } from "@api/APIClient";
|
||||
import { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, SwitchGroupWideRed, TextFieldWide } from "@components/inputs";
|
||||
import { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "@components/inputs";
|
||||
import { SlideOver } from "@components/panels";
|
||||
import Toast from "@components/notifications/Toast";
|
||||
import * as common from "@components/inputs/common";
|
||||
import { classNames } from "@utils";
|
||||
|
||||
interface ChannelsFieldArrayProps {
|
||||
channels: IrcChannel[];
|
||||
}
|
||||
|
||||
const ChannelsFieldArray = ({ channels }: ChannelsFieldArrayProps) => (
|
||||
<div className="p-6">
|
||||
<div className="px-4">
|
||||
<FieldArray name="channels">
|
||||
{({ remove, push }: FieldArrayRenderProps) => (
|
||||
<div className="flex flex-col space-y-2 border-2 border-dashed dark:border-gray-700 p-4">
|
||||
<div className="flex flex-col space-y-2">
|
||||
{channels && channels.length > 0 ? (
|
||||
channels.map((_channel: IrcChannel, index: number) => {
|
||||
const isDisabled = channels[index].name === "#ptp-announce-dev";
|
||||
channels.map((channel: IrcChannel, index) => {
|
||||
const isDisabled = channel.name === "#ptp-announce-dev";
|
||||
return (
|
||||
<div key={index} className="flex justify-between">
|
||||
<div className="flex">
|
||||
<div key={index} className="flex justify-between border dark:border-gray-700 dark:bg-gray-815 p-2 rounded-md">
|
||||
<div className="flex gap-2">
|
||||
<Field name={`channels.${index}.name`}>
|
||||
{({ field }: FieldProps) => (
|
||||
{({ field, meta }: FieldProps) => (
|
||||
<input
|
||||
{...field}
|
||||
type="text"
|
||||
value={field.value ?? ""}
|
||||
onChange={field.onChange}
|
||||
placeholder="#Channel"
|
||||
className={`mr-4 focus:ring-blue-500 dark:focus:ring-blue-500 focus:border-blue-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-600 block w-full shadow-sm sm:text-sm rounded-md
|
||||
${isDisabled ? "disabled dark:bg-gray-800 dark:text-gray-500" : "dark:bg-gray-700 dark:text-white"}`}
|
||||
className={classNames(
|
||||
meta.touched && meta.error
|
||||
? "border-red-500 focus:ring-red-500 focus:border-red-500"
|
||||
: "border-gray-300 dark:border-gray-700 focus:ring-blue-500 dark:focus:ring-blue-500 focus:border-blue-500 dark:focus:border-blue-500",
|
||||
"block w-full shadow-sm sm:text-sm rounded-md border py-2.5",
|
||||
isDisabled ? "disabled dark:bg-gray-700 dark:text-gray-400 cursor-not-allowed" : "bg-gray-100 dark:bg-gray-850 dark:text-gray-100"
|
||||
)}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Field name={`channels.${index}.password`}>
|
||||
{({ field }: FieldProps) => (
|
||||
{({ field, meta }: FieldProps) => (
|
||||
<input
|
||||
{...field}
|
||||
type="text"
|
||||
value={field.value ?? ""}
|
||||
onChange={field.onChange}
|
||||
placeholder="Password"
|
||||
className={`mr-4 focus:ring-blue-500 dark:focus:ring-blue-500 focus:border-blue-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-600 block w-full shadow-sm sm:text-sm rounded-md
|
||||
${isDisabled ? "disabled dark:bg-gray-800 dark:text-gray-500" : "dark:bg-gray-700 dark:text-white"}`}
|
||||
placeholder="Channel password"
|
||||
className={classNames(
|
||||
meta.touched && meta.error
|
||||
? "border-red-500 focus:ring-red-500 focus:border-red-500"
|
||||
: "border-gray-300 dark:border-gray-700 focus:ring-blue-500 dark:focus:ring-blue-500 focus:border-blue-500 dark:focus:border-blue-500",
|
||||
"block w-full shadow-sm sm:text-sm rounded-md border py-2.5",
|
||||
isDisabled ? "disabled dark:bg-gray-700 dark:text-white cursor-not-allowed" : "bg-gray-100 dark:bg-gray-850 dark:text-gray-100"
|
||||
)}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
)}
|
||||
|
@ -68,8 +79,10 @@ const ChannelsFieldArray = ({ channels }: ChannelsFieldArrayProps) => (
|
|||
|
||||
<button
|
||||
type="button"
|
||||
className={`bg-white dark:bg-gray-700 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-500
|
||||
${isDisabled ? "disabled hidden" : ""}`}
|
||||
className={classNames(
|
||||
"bg-white dark:bg-gray-700 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-500",
|
||||
isDisabled ? "hidden" : ""
|
||||
)}
|
||||
onClick={() => remove(index)}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
|
@ -204,7 +217,16 @@ export function IrcNetworkAddForm({ isOpen, toggle }: AddFormProps) {
|
|||
/>
|
||||
<PasswordFieldWide name="invite_command" label="Invite command" />
|
||||
|
||||
<ChannelsFieldArray channels={values.channels} />
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
|
||||
<div className="px-4 space-y-1 mb-8">
|
||||
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Channels</Dialog.Title>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Channels to join.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ChannelsFieldArray channels={values.channels} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</SlideOver>
|
||||
|
@ -324,7 +346,7 @@ export function IrcNetworkUpdateForm({
|
|||
required={true}
|
||||
/>
|
||||
|
||||
<SwitchGroupWideRed name="enabled" label="Enabled" />
|
||||
<SwitchGroupWide name="enabled" label="Enabled" />
|
||||
<TextFieldWide
|
||||
name="server"
|
||||
label="Server"
|
||||
|
@ -388,12 +410,20 @@ export function IrcNetworkUpdateForm({
|
|||
label="Password"
|
||||
help="NickServ / SASL password."
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
<PasswordFieldWide name="invite_command" label="Invite command" />
|
||||
|
||||
<ChannelsFieldArray channels={values.channels} />
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
|
||||
<div className="px-4 space-y-1 mb-8">
|
||||
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Channels</Dialog.Title>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Channels are added when you setup IRC indexers. Do not edit unless you know what you are doing.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ChannelsFieldArray channels={values.channels} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</SlideOver>
|
||||
|
@ -429,10 +459,12 @@ function SelectField<T>({ name, label, options }: SelectFieldProps<T>) {
|
|||
isClearable={true}
|
||||
isSearchable={true}
|
||||
components={{
|
||||
Input,
|
||||
Control,
|
||||
Menu,
|
||||
Option
|
||||
Input: common.SelectInput,
|
||||
Control: common.SelectControl,
|
||||
Menu: common.SelectMenu,
|
||||
Option: common.SelectOption,
|
||||
IndicatorSeparator: common.IndicatorSeparator,
|
||||
DropdownIndicator: common.DropdownIndicator
|
||||
}}
|
||||
placeholder="Choose a type"
|
||||
styles={{
|
||||
|
@ -468,44 +500,3 @@ function SelectField<T>({ name, label, options }: SelectFieldProps<T>) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Input = (props: InputProps) => {
|
||||
return (
|
||||
<components.Input
|
||||
{...props}
|
||||
inputClassName="outline-none border-none shadow-none focus:ring-transparent"
|
||||
className="text-gray-400 dark:text-gray-100"
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Control = (props: ControlProps) => {
|
||||
return (
|
||||
<components.Control
|
||||
{...props}
|
||||
className="p-1 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Menu = (props: MenuProps) => {
|
||||
return (
|
||||
<components.Menu
|
||||
{...props}
|
||||
className="dark:bg-gray-800 border border-gray-300 dark:border-gray-700 dark:text-gray-400 rounded-md shadow-sm"
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Option = (props: OptionProps) => {
|
||||
return (
|
||||
<components.Option
|
||||
{...props}
|
||||
className="dark:text-gray-400 dark:bg-gray-800 dark:hover:bg-gray-900 dark:focus:bg-gray-900"
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,60 +8,21 @@ import { Fragment } from "react";
|
|||
import type { FieldProps } from "formik";
|
||||
import { Field, Form, Formik, FormikErrors, FormikValues } from "formik";
|
||||
import { XMarkIcon } from "@heroicons/react/24/solid";
|
||||
import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select";
|
||||
import Select from "react-select";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
import { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "@components/inputs";
|
||||
import DEBUG from "@components/debug";
|
||||
import { EventOptions, NotificationTypeOptions, SelectOption } from "@domain/constants";
|
||||
import { APIClient } from "@api/APIClient";
|
||||
import Toast from "@components/notifications/Toast";
|
||||
import { SlideOver } from "@components/panels";
|
||||
import { componentMapType } from "./DownloadClientForms";
|
||||
import { notificationKeys } from "@screens/settings/Notifications";
|
||||
import { EventOptions, NotificationTypeOptions, SelectOption } from "@domain/constants";
|
||||
import DEBUG from "@components/debug";
|
||||
import { SlideOver } from "@components/panels";
|
||||
import { ExternalLink } from "@components/ExternalLink";
|
||||
import Toast from "@components/notifications/Toast";
|
||||
import * as common from "@components/inputs/common";
|
||||
import { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "@components/inputs";
|
||||
|
||||
const Input = (props: InputProps) => {
|
||||
return (
|
||||
<components.Input
|
||||
{...props}
|
||||
inputClassName="outline-none border-none shadow-none focus:ring-transparent"
|
||||
className="text-gray-400 dark:text-gray-100"
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Control = (props: ControlProps) => {
|
||||
return (
|
||||
<components.Control
|
||||
{...props}
|
||||
className="p-1 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Menu = (props: MenuProps) => {
|
||||
return (
|
||||
<components.Menu
|
||||
{...props}
|
||||
className="dark:bg-gray-800 border border-gray-300 dark:border-gray-700 dark:text-gray-400 rounded-md shadow-sm"
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Option = (props: OptionProps) => {
|
||||
return (
|
||||
<components.Option
|
||||
{...props}
|
||||
className="dark:text-gray-400 dark:bg-gray-800 dark:hover:bg-gray-900 dark:focus:bg-gray-900"
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
};
|
||||
import { componentMapType } from "./DownloadClientForms";
|
||||
|
||||
function FormFieldsDiscord() {
|
||||
return (
|
||||
|
@ -295,7 +256,7 @@ export function NotificationAddForm({ isOpen, toggle }: AddProps) {
|
|||
validate={validate}
|
||||
>
|
||||
{({ 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-auto">
|
||||
<div className="flex-1">
|
||||
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
|
||||
<div className="flex items-start justify-between space-x-3">
|
||||
|
@ -347,10 +308,12 @@ export function NotificationAddForm({ isOpen, toggle }: AddProps) {
|
|||
isClearable={true}
|
||||
isSearchable={true}
|
||||
components={{
|
||||
Input,
|
||||
Control,
|
||||
Menu,
|
||||
Option
|
||||
Input: common.SelectInput,
|
||||
Control: common.SelectControl,
|
||||
Menu: common.SelectMenu,
|
||||
Option: common.SelectOption,
|
||||
IndicatorSeparator: common.IndicatorSeparator,
|
||||
DropdownIndicator: common.DropdownIndicator
|
||||
}}
|
||||
placeholder="Choose a type"
|
||||
styles={{
|
||||
|
@ -574,8 +537,14 @@ export function NotificationUpdateForm({ isOpen, toggle, notification }: UpdateP
|
|||
<Select {...field}
|
||||
isClearable={true}
|
||||
isSearchable={true}
|
||||
components={{ Input, Control, Menu, Option }}
|
||||
|
||||
components={{
|
||||
Input: common.SelectInput,
|
||||
Control: common.SelectControl,
|
||||
Menu: common.SelectMenu,
|
||||
Option: common.SelectOption,
|
||||
IndicatorSeparator: common.IndicatorSeparator,
|
||||
DropdownIndicator: common.DropdownIndicator
|
||||
}}
|
||||
placeholder="Choose a type"
|
||||
styles={{
|
||||
singleValue: (base) => ({
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue