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:
stacksmash76 2023-11-18 13:46:16 +00:00 committed by GitHub
parent a274d9ddce
commit e842a7bd42
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
84 changed files with 4378 additions and 4361 deletions

View file

@ -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 &&

View file

@ -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>

View file

@ -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>

View file

@ -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 }))}
/>
)}

View file

@ -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}
/>
);
};

View file

@ -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) => ({