163 lines
5.7 KiB
TypeScript
163 lines
5.7 KiB
TypeScript
"use client";
|
|
|
|
export type AdminSetting = { key: string; value: string | null; isSet: boolean; source: string; sensitive: boolean };
|
|
type Option = { value: string; label: string };
|
|
type Props = {
|
|
setting: AdminSetting;
|
|
label: string;
|
|
value: string;
|
|
help?: string;
|
|
placeholder?: string;
|
|
boolean?: boolean;
|
|
numeric?: boolean;
|
|
multiline?: boolean;
|
|
options?: Option[];
|
|
optionsUnavailable?: boolean;
|
|
onChange: (value: string) => void;
|
|
};
|
|
|
|
const SELECTS: Record<string, Option[]> = {
|
|
log_level: ["DEBUG", "INFO", "WARNING", "ERROR"].map((value) => ({ value, label: value })),
|
|
issue_confirmation_contact_attempts: Array.from({ length: 11 }, (_, index) => ({
|
|
value: String(index),
|
|
label: index === 0 ? "None — close when fixed" : String(index),
|
|
})),
|
|
issue_confirmation_interval_unit: ["days", "weeks", "months"].map((value) => ({
|
|
value,
|
|
label: value[0].toUpperCase() + value.slice(1),
|
|
})),
|
|
artwork_cache_mode: [
|
|
{ value: "remote", label: "Load from the internet" },
|
|
{ value: "cache", label: "Store locally" },
|
|
],
|
|
site_banner_tone: ["info", "warning", "error", "maintenance"].map((value) => ({
|
|
value,
|
|
label: value[0].toUpperCase() + value.slice(1),
|
|
})),
|
|
magent_notify_push_provider: ["ntfy", "gotify", "pushover", "webhook", "telegram", "discord"].map((value) => ({
|
|
value,
|
|
label: value,
|
|
})),
|
|
requests_data_source: [
|
|
{ value: "always_js", label: "Read directly from Seerr" },
|
|
{ value: "prefer_cache", label: "Use saved requests" },
|
|
],
|
|
};
|
|
|
|
const COLOR_DEFAULTS: Record<string, string> = {
|
|
site_banner_background_color: "#332814",
|
|
site_banner_border_color: "#a27b32",
|
|
};
|
|
|
|
export default function SettingField(props: Props) {
|
|
const { setting, label, value, help, placeholder, onChange } = props;
|
|
const id = `setting-${setting.key}`;
|
|
const options =
|
|
props.options ??
|
|
SELECTS[setting.key] ??
|
|
(setting.key === "log_http_client_level" || setting.key === "log_background_sync_level"
|
|
? SELECTS.log_level
|
|
: undefined);
|
|
const selectedOptions =
|
|
options && value && !options.some((option) => option.value === value)
|
|
? [{ value, label: `Current selection (${value})` }, ...options]
|
|
: options;
|
|
const isTime = setting.key === "requests_full_sync_time" || setting.key === "requests_cleanup_time";
|
|
const zeroAllowed = setting.key === "log_file_backup_count";
|
|
const minimum = zeroAllowed ? 0 : 1;
|
|
const maximum =
|
|
setting.key === "issue_confirmation_interval_value" ? 365 : setting.key.endsWith("_port") ? 65535 : undefined;
|
|
const colorDefault = COLOR_DEFAULTS[setting.key];
|
|
const pickerValue = /^#[0-9a-f]{6}$/i.test(value) ? value : colorDefault;
|
|
const aria = { id, name: setting.key, "aria-describedby": help ? `${id}-help` : undefined };
|
|
|
|
if (props.boolean) {
|
|
return (
|
|
<div className="setting-field setting-switch">
|
|
<div>
|
|
<label htmlFor={id}>{label}</label>
|
|
{help && <p id={`${id}-help`}>{help}</p>}
|
|
</div>
|
|
<input
|
|
{...aria}
|
|
type="checkbox"
|
|
role="switch"
|
|
aria-checked={value.toLowerCase() === "true"}
|
|
checked={value.toLowerCase() === "true"}
|
|
onChange={(event) => onChange(String(event.target.checked))}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={`setting-field ${props.multiline ? "field-span-full" : ""}`}>
|
|
<label htmlFor={id}>
|
|
{label}
|
|
{setting.sensitive && setting.isSet && <small>Saved</small>}
|
|
</label>
|
|
{props.optionsUnavailable ? (
|
|
<select {...aria} disabled value={value}>
|
|
<option value={value}>Save the connection, then reload available options</option>
|
|
</select>
|
|
) : colorDefault ? (
|
|
<div className="setting-color-control">
|
|
<input
|
|
id={`${id}-picker`}
|
|
type="color"
|
|
aria-label={`Choose ${label.toLowerCase()}`}
|
|
value={pickerValue}
|
|
onChange={(event) => onChange(event.target.value)}
|
|
/>
|
|
<input
|
|
{...aria}
|
|
type="text"
|
|
value={value}
|
|
pattern="#[0-9A-Fa-f]{6}"
|
|
placeholder={colorDefault}
|
|
autoComplete="off"
|
|
spellCheck={false}
|
|
onChange={(event) => onChange(event.target.value)}
|
|
/>
|
|
{value ? (
|
|
<button type="button" className="ghost-button" onClick={() => onChange("")}>
|
|
Use tone default
|
|
</button>
|
|
) : null}
|
|
</div>
|
|
) : selectedOptions ? (
|
|
<select {...aria} value={value} onChange={(event) => onChange(event.target.value)}>
|
|
{!value && <option value="">Choose an option</option>}
|
|
{selectedOptions.map((option) => (
|
|
<option key={option.value} value={option.value}>
|
|
{option.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
) : props.multiline ? (
|
|
<textarea
|
|
{...aria}
|
|
rows={setting.key.includes("_pem") ? 6 : 3}
|
|
value={value}
|
|
placeholder={placeholder}
|
|
onChange={(event) => onChange(event.target.value)}
|
|
/>
|
|
) : (
|
|
<input
|
|
{...aria}
|
|
type={setting.sensitive ? "password" : props.numeric ? "number" : isTime ? "time" : "text"}
|
|
value={value}
|
|
min={props.numeric ? minimum : undefined}
|
|
max={props.numeric ? maximum : undefined}
|
|
step={props.numeric ? 1 : undefined}
|
|
autoComplete={setting.sensitive ? "new-password" : "off"}
|
|
spellCheck={false}
|
|
placeholder={setting.sensitive && setting.isSet ? "Leave blank to keep the saved value" : placeholder}
|
|
onChange={(event) => onChange(event.target.value)}
|
|
/>
|
|
)}
|
|
{help && <p id={`${id}-help`}>{help}</p>}
|
|
</div>
|
|
);
|
|
}
|