fix(ui): improve form usability (#1563)

* fix(ui): improve form usability

* refactor: remove unnecessary <> and </> tags

* fix(ui): set url inputmode for *arr URL base fields
This commit is contained in:
TheCatLady
2021-05-04 04:42:27 -04:00
committed by GitHub
parent b05b177776
commit 26580eaa21
19 changed files with 598 additions and 634 deletions

View File

@@ -15,7 +15,8 @@ const messages = defineMessages({
botUsername: 'Bot Username',
botAvatarUrl: 'Bot Avatar URL',
webhookUrl: 'Webhook URL',
webhookUrlPlaceholder: 'Server Settings → Integrations → Webhooks',
webhookUrlTip:
'Create a <DiscordWebhookLink>webhook integration</DiscordWebhookLink> in your server',
discordsettingssaved: 'Discord notification settings saved successfully!',
discordsettingsfailed: 'Discord notification settings failed to save.',
toastDiscordTestSending: 'Sending Discord test notification…',
@@ -137,23 +138,54 @@ const NotificationsDiscord: React.FC = () => {
<div className="form-row">
<label htmlFor="enabled" className="checkbox-label">
{intl.formatMessage(messages.agentenabled)}
<span className="label-required">*</span>
</label>
<div className="form-input">
<Field type="checkbox" id="enabled" name="enabled" />
</div>
</div>
<div className="form-row">
<label htmlFor="name" className="text-label">
{intl.formatMessage(messages.webhookUrl)}
<span className="label-required">*</span>
<span className="label-tip">
{intl.formatMessage(messages.webhookUrlTip, {
DiscordWebhookLink: function DiscordWebhookLink(msg) {
return (
<a
href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
})}
</span>
</label>
<div className="form-input">
<div className="form-input-field">
<Field
id="webhookUrl"
name="webhookUrl"
type="text"
inputMode="url"
/>
</div>
{errors.webhookUrl && touched.webhookUrl && (
<div className="error">{errors.webhookUrl}</div>
)}
</div>
</div>
<div className="form-row">
<label htmlFor="botUsername" className="text-label">
{intl.formatMessage(messages.botUsername)}
</label>
<div className="form-input">
<div className="form-input-field">
<Field
id="botUsername"
name="botUsername"
type="text"
placeholder={intl.formatMessage(messages.botUsername)}
/>
<Field id="botUsername" name="botUsername" type="text" />
</div>
{errors.botUsername && touched.botUsername && (
<div className="error">{errors.botUsername}</div>
@@ -170,7 +202,7 @@ const NotificationsDiscord: React.FC = () => {
id="botAvatarUrl"
name="botAvatarUrl"
type="text"
placeholder={intl.formatMessage(messages.botAvatarUrl)}
inputMode="url"
/>
</div>
{errors.botAvatarUrl && touched.botAvatarUrl && (
@@ -178,27 +210,6 @@ const NotificationsDiscord: React.FC = () => {
)}
</div>
</div>
<div className="form-row">
<label htmlFor="name" className="text-label">
{intl.formatMessage(messages.webhookUrl)}
<span className="label-required">*</span>
</label>
<div className="form-input">
<div className="form-input-field">
<Field
id="webhookUrl"
name="webhookUrl"
type="text"
placeholder={intl.formatMessage(
messages.webhookUrlPlaceholder
)}
/>
</div>
{errors.webhookUrl && touched.webhookUrl && (
<div className="error">{errors.webhookUrl}</div>
)}
</div>
</div>
<NotificationTypeSelector
currentTypes={values.types}
onUpdate={(newTypes) => setFieldValue('types', newTypes)}

View File

@@ -131,7 +131,7 @@ const NotificationsEmail: React.FC = () => {
types: data.types,
emailFrom: data.options.emailFrom,
smtpHost: data.options.smtpHost,
smtpPort: data.options.smtpPort,
smtpPort: data.options.smtpPort ?? 587,
secure: data.options.secure,
authUser: data.options.authUser,
authPass: data.options.authPass,
@@ -266,11 +266,22 @@ const NotificationsEmail: React.FC = () => {
<div className="form-row">
<label htmlFor="enabled" className="checkbox-label">
{intl.formatMessage(messages.agentenabled)}
<span className="label-required">*</span>
</label>
<div className="form-input">
<Field type="checkbox" id="enabled" name="enabled" />
</div>
</div>
<div className="form-row">
<label htmlFor="senderName" className="text-label">
{intl.formatMessage(messages.senderName)}
</label>
<div className="form-input">
<div className="form-input-field">
<Field id="senderName" name="senderName" type="text" />
</div>
</div>
</div>
<div className="form-row">
<label htmlFor="emailFrom" className="text-label">
{intl.formatMessage(messages.emailsender)}
@@ -282,7 +293,7 @@ const NotificationsEmail: React.FC = () => {
id="emailFrom"
name="emailFrom"
type="text"
placeholder="no-reply@example.com"
inputMode="email"
/>
</div>
{errors.emailFrom && touched.emailFrom && (
@@ -290,21 +301,6 @@ const NotificationsEmail: React.FC = () => {
)}
</div>
</div>
<div className="form-row">
<label htmlFor="senderName" className="text-label">
{intl.formatMessage(messages.senderName)}
</label>
<div className="form-input">
<div className="form-input-field">
<Field
id="senderName"
name="senderName"
placeholder="Overseerr"
type="text"
/>
</div>
</div>
</div>
<div className="form-row">
<label htmlFor="smtpHost" className="text-label">
{intl.formatMessage(messages.smtpHost)}
@@ -316,7 +312,7 @@ const NotificationsEmail: React.FC = () => {
id="smtpHost"
name="smtpHost"
type="text"
placeholder="localhost"
inputMode="url"
/>
</div>
{errors.smtpHost && touched.smtpHost && (
@@ -334,7 +330,7 @@ const NotificationsEmail: React.FC = () => {
id="smtpPort"
name="smtpPort"
type="text"
placeholder="465"
inputMode="numeric"
className="short"
/>
{errors.smtpPort && touched.smtpPort && (
@@ -385,8 +381,7 @@ const NotificationsEmail: React.FC = () => {
as="field"
id="authPass"
name="authPass"
type="password"
autoComplete="off"
autoComplete="one-time-code"
/>
</div>
</div>
@@ -441,8 +436,7 @@ const NotificationsEmail: React.FC = () => {
as="field"
id="pgpPassword"
name="pgpPassword"
type="password"
autoComplete="off"
autoComplete="one-time-code"
/>
</div>
{errors.pgpPassword && touched.pgpPassword && (

View File

@@ -13,6 +13,8 @@ import NotificationTypeSelector from '../../../NotificationTypeSelector';
const messages = defineMessages({
agentenabled: 'Enable Agent',
webhookUrl: 'Webhook URL',
webhookUrlTip:
'Your user- or device-based <LunaSeaLink>notification webhook URL</LunaSeaLink>',
validationWebhookUrl: 'You must provide a valid URL',
profileName: 'Profile Name',
profileNameTip: 'Only required if not using the <code>default</code> profile',
@@ -129,6 +131,7 @@ const NotificationsLunaSea: React.FC = () => {
<div className="form-row">
<label htmlFor="enabled" className="checkbox-label">
{intl.formatMessage(messages.agentenabled)}
<span className="label-required">*</span>
</label>
<div className="form-input">
<Field type="checkbox" id="enabled" name="enabled" />
@@ -138,10 +141,31 @@ const NotificationsLunaSea: React.FC = () => {
<label htmlFor="name" className="text-label">
{intl.formatMessage(messages.webhookUrl)}
<span className="label-required">*</span>
<span className="label-tip">
{intl.formatMessage(messages.webhookUrlTip, {
LunaSeaLink: function LunaSeaLink(msg) {
return (
<a
href="https://docs.lunasea.app/lunasea/notifications/overseerr"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
})}
</span>
</label>
<div className="form-input">
<div className="form-input-field">
<Field id="webhookUrl" name="webhookUrl" type="text" />
<Field
id="webhookUrl"
name="webhookUrl"
type="text"
inputMode="url"
/>
</div>
{errors.webhookUrl && touched.webhookUrl && (
<div className="error">{errors.webhookUrl}</div>

View File

@@ -6,7 +6,6 @@ import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import globalMessages from '../../../../i18n/globalMessages';
import Alert from '../../../Common/Alert';
import Button from '../../../Common/Button';
import LoadingSpinner from '../../../Common/LoadingSpinner';
import SensitiveInput from '../../../Common/SensitiveInput';
@@ -15,6 +14,8 @@ import NotificationTypeSelector from '../../../NotificationTypeSelector';
const messages = defineMessages({
agentEnabled: 'Enable Agent',
accessToken: 'Access Token',
accessTokenTip:
'Create a token from your <PushbulletSettingsLink>Account Settings</PushbulletSettingsLink>',
validationAccessTokenRequired: 'You must provide an access token',
pushbulletSettingsSaved:
'Pushbullet notification settings saved successfully!',
@@ -22,8 +23,6 @@ const messages = defineMessages({
toastPushbulletTestSending: 'Sending Pushbullet test notification…',
toastPushbulletTestSuccess: 'Pushbullet test notification sent!',
toastPushbulletTestFailed: 'Pushbullet test notification failed to send.',
settingUpPushbulletDescription:
'To configure Pushbullet notifications, you will need to <CreateAccessTokenLink>create an access token</CreateAccessTokenLink>.',
});
const NotificationsPushbullet: React.FC = () => {
@@ -123,91 +122,87 @@ const NotificationsPushbullet: React.FC = () => {
};
return (
<>
<Alert
title={intl.formatMessage(
messages.settingUpPushbulletDescription,
{
CreateAccessTokenLink: function CreateAccessTokenLink(msg) {
return (
<a
href="https://www.pushbullet.com/#settings"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
}
)}
type="info"
<Form className="section">
<div className="form-row">
<label htmlFor="enabled" className="checkbox-label">
{intl.formatMessage(messages.agentEnabled)}
<span className="label-required">*</span>
</label>
<div className="form-input">
<Field type="checkbox" id="enabled" name="enabled" />
</div>
</div>
<div className="form-row">
<label htmlFor="accessToken" className="text-label">
{intl.formatMessage(messages.accessToken)}
<span className="label-required">*</span>
<span className="label-tip">
{intl.formatMessage(messages.accessTokenTip, {
PushbulletSettingsLink: function PushbulletSettingsLink(
msg
) {
return (
<a
href="https://www.pushbullet.com/#settings/account"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
})}
</span>
</label>
<div className="form-input">
<div className="form-input-field">
<SensitiveInput
as="field"
id="accessToken"
name="accessToken"
autoComplete="one-time-code"
/>
</div>
{errors.accessToken && touched.accessToken && (
<div className="error">{errors.accessToken}</div>
)}
</div>
</div>
<NotificationTypeSelector
currentTypes={values.types}
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
/>
<Form className="section">
<div className="form-row">
<label htmlFor="enabled" className="checkbox-label">
{intl.formatMessage(messages.agentEnabled)}
</label>
<div className="form-input">
<Field type="checkbox" id="enabled" name="enabled" />
</div>
<div className="actions">
<div className="flex justify-end">
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="warning"
disabled={isSubmitting || !isValid || isTesting}
onClick={(e) => {
e.preventDefault();
testSettings();
}}
>
{isTesting
? intl.formatMessage(globalMessages.testing)
: intl.formatMessage(globalMessages.test)}
</Button>
</span>
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="primary"
type="submit"
disabled={isSubmitting || !isValid || isTesting}
>
{isSubmitting
? intl.formatMessage(globalMessages.saving)
: intl.formatMessage(globalMessages.save)}
</Button>
</span>
</div>
<div className="form-row">
<label htmlFor="accessToken" className="text-label">
{intl.formatMessage(messages.accessToken)}
<span className="label-required">*</span>
</label>
<div className="form-input">
<div className="form-input-field">
<SensitiveInput
as="field"
id="accessToken"
name="accessToken"
type="text"
placeholder={intl.formatMessage(messages.accessToken)}
/>
</div>
{errors.accessToken && touched.accessToken && (
<div className="error">{errors.accessToken}</div>
)}
</div>
</div>
<NotificationTypeSelector
currentTypes={values.types}
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
/>
<div className="actions">
<div className="flex justify-end">
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="warning"
disabled={isSubmitting || !isValid || isTesting}
onClick={(e) => {
e.preventDefault();
testSettings();
}}
>
{isTesting
? intl.formatMessage(globalMessages.testing)
: intl.formatMessage(globalMessages.test)}
</Button>
</span>
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="primary"
type="submit"
disabled={isSubmitting || !isValid || isTesting}
>
{isSubmitting
? intl.formatMessage(globalMessages.saving)
: intl.formatMessage(globalMessages.save)}
</Button>
</span>
</div>
</div>
</Form>
</>
</div>
</Form>
);
}}
</Formik>

View File

@@ -6,15 +6,18 @@ import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import globalMessages from '../../../../i18n/globalMessages';
import Alert from '../../../Common/Alert';
import Button from '../../../Common/Button';
import LoadingSpinner from '../../../Common/LoadingSpinner';
import NotificationTypeSelector from '../../../NotificationTypeSelector';
const messages = defineMessages({
agentenabled: 'Enable Agent',
accessToken: 'Application/API Token',
accessToken: 'Application API Token',
accessTokenTip:
'<ApplicationRegistrationLink>Register an application</ApplicationRegistrationLink> for use with Overseerr',
userToken: 'User or Group Key',
userTokenTip:
'Your 30-character <UsersGroupsLink>user or group identifier</UsersGroupsLink>',
validationAccessTokenRequired: 'You must provide a valid application token',
validationUserTokenRequired: 'You must provide a valid user key',
pushoversettingssaved: 'Pushover notification settings saved successfully!',
@@ -22,8 +25,6 @@ const messages = defineMessages({
toastPushoverTestSending: 'Sending Pushover test notification…',
toastPushoverTestSuccess: 'Pushover test notification sent!',
toastPushoverTestFailed: 'Pushover test notification failed to send.',
settinguppushoverDescription:
'To configure Pushover notifications, you will need to <RegisterApplicationLink>register an application</RegisterApplicationLink>. (You can use one of the <IconLink>official Overseerr icons on GitHub</IconLink>.)',
});
const NotificationsPushover: React.FC = () => {
@@ -143,118 +144,112 @@ const NotificationsPushover: React.FC = () => {
};
return (
<>
<Alert
title={intl.formatMessage(messages.settinguppushoverDescription, {
RegisterApplicationLink: function RegisterApplicationLink(msg) {
return (
<a
href="https://pushover.net/apps/build"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
IconLink: function IconLink(msg) {
return (
<a
href="https://github.com/sct/overseerr/tree/develop/public"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
})}
type="info"
<Form className="section">
<div className="form-row">
<label htmlFor="enabled" className="checkbox-label">
{intl.formatMessage(messages.agentenabled)}
<span className="label-required">*</span>
</label>
<div className="form-input">
<Field type="checkbox" id="enabled" name="enabled" />
</div>
</div>
<div className="form-row">
<label htmlFor="accessToken" className="text-label">
{intl.formatMessage(messages.accessToken)}
<span className="label-required">*</span>
<span className="label-tip">
{intl.formatMessage(messages.accessTokenTip, {
ApplicationRegistrationLink: function ApplicationRegistrationLink(
msg
) {
return (
<a
href="https://pushover.net/api#registration"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
})}
</span>
</label>
<div className="form-input">
<div className="form-input-field">
<Field id="accessToken" name="accessToken" type="text" />
</div>
{errors.accessToken && touched.accessToken && (
<div className="error">{errors.accessToken}</div>
)}
</div>
</div>
<div className="form-row">
<label htmlFor="userToken" className="text-label">
{intl.formatMessage(messages.userToken)}
<span className="label-required">*</span>
<span className="label-tip">
{intl.formatMessage(messages.userTokenTip, {
UsersGroupsLink: function UsersGroupsLink(msg) {
return (
<a
href="https://pushover.net/api#identifiers"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
})}
</span>
</label>
<div className="form-input">
<div className="form-input-field">
<Field id="userToken" name="userToken" type="text" />
</div>
{errors.userToken && touched.userToken && (
<div className="error">{errors.userToken}</div>
)}
</div>
</div>
<NotificationTypeSelector
currentTypes={values.types}
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
/>
<Form className="section">
<div className="form-row">
<label htmlFor="enabled" className="checkbox-label">
{intl.formatMessage(messages.agentenabled)}
</label>
<div className="form-input">
<Field type="checkbox" id="enabled" name="enabled" />
</div>
<div className="actions">
<div className="flex justify-end">
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="warning"
disabled={isSubmitting || !isValid || isTesting}
onClick={(e) => {
e.preventDefault();
testSettings();
}}
>
{isTesting
? intl.formatMessage(globalMessages.testing)
: intl.formatMessage(globalMessages.test)}
</Button>
</span>
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="primary"
type="submit"
disabled={isSubmitting || !isValid || isTesting}
>
{isSubmitting
? intl.formatMessage(globalMessages.saving)
: intl.formatMessage(globalMessages.save)}
</Button>
</span>
</div>
<div className="form-row">
<label htmlFor="accessToken" className="text-label">
{intl.formatMessage(messages.accessToken)}
<span className="label-required">*</span>
</label>
<div className="form-input">
<div className="form-input-field">
<Field
id="accessToken"
name="accessToken"
type="text"
placeholder={intl.formatMessage(messages.accessToken)}
/>
</div>
{errors.accessToken && touched.accessToken && (
<div className="error">{errors.accessToken}</div>
)}
</div>
</div>
<div className="form-row">
<label htmlFor="userToken" className="text-label">
{intl.formatMessage(messages.userToken)}
<span className="label-required">*</span>
</label>
<div className="form-input">
<div className="form-input-field">
<Field
id="userToken"
name="userToken"
type="text"
placeholder={intl.formatMessage(messages.userToken)}
/>
</div>
{errors.userToken && touched.userToken && (
<div className="error">{errors.userToken}</div>
)}
</div>
</div>
<NotificationTypeSelector
currentTypes={values.types}
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
/>
<div className="actions">
<div className="flex justify-end">
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="warning"
disabled={isSubmitting || !isValid || isTesting}
onClick={(e) => {
e.preventDefault();
testSettings();
}}
>
{isTesting
? intl.formatMessage(globalMessages.testing)
: intl.formatMessage(globalMessages.test)}
</Button>
</span>
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="primary"
type="submit"
disabled={isSubmitting || !isValid || isTesting}
>
{isSubmitting
? intl.formatMessage(globalMessages.saving)
: intl.formatMessage(globalMessages.save)}
</Button>
</span>
</div>
</div>
</Form>
</>
</div>
</Form>
);
}}
</Formik>

View File

@@ -6,7 +6,6 @@ import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import globalMessages from '../../../../i18n/globalMessages';
import Alert from '../../../Common/Alert';
import Button from '../../../Common/Button';
import LoadingSpinner from '../../../Common/LoadingSpinner';
import NotificationTypeSelector from '../../../NotificationTypeSelector';
@@ -14,13 +13,13 @@ import NotificationTypeSelector from '../../../NotificationTypeSelector';
const messages = defineMessages({
agentenabled: 'Enable Agent',
webhookUrl: 'Webhook URL',
webhookUrlTip:
'Create an <WebhookLink>Incoming Webhook</WebhookLink> integration',
slacksettingssaved: 'Slack notification settings saved successfully!',
slacksettingsfailed: 'Slack notification settings failed to save.',
toastSlackTestSending: 'Sending Slack test notification…',
toastSlackTestSuccess: 'Slack test notification sent!',
toastSlackTestFailed: 'Slack test notification failed to send.',
settingupslackDescription:
'To configure Slack notifications, you will need to create an <WebhookLink>Incoming Webhook</WebhookLink> integration and enter the webhook URL below.',
validationWebhookUrl: 'You must provide a valid URL',
});
@@ -49,166 +48,162 @@ const NotificationsSlack: React.FC = () => {
}
return (
<>
<Alert
title={intl.formatMessage(messages.settingupslackDescription, {
WebhookLink: function WebhookLink(msg) {
return (
<a
href="https://my.slack.com/services/new/incoming-webhook/"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
})}
type="info"
/>
<Formik
initialValues={{
enabled: data.enabled,
types: data.types,
webhookUrl: data.options.webhookUrl,
}}
validationSchema={NotificationsSlackSchema}
onSubmit={async (values) => {
<Formik
initialValues={{
enabled: data.enabled,
types: data.types,
webhookUrl: data.options.webhookUrl,
}}
validationSchema={NotificationsSlackSchema}
onSubmit={async (values) => {
try {
await axios.post('/api/v1/settings/notifications/slack', {
enabled: values.enabled,
types: values.types,
options: {
webhookUrl: values.webhookUrl,
},
});
addToast(intl.formatMessage(messages.slacksettingssaved), {
appearance: 'success',
autoDismiss: true,
});
} catch (e) {
addToast(intl.formatMessage(messages.slacksettingsfailed), {
appearance: 'error',
autoDismiss: true,
});
} finally {
revalidate();
}
}}
>
{({ errors, touched, isSubmitting, values, isValid, setFieldValue }) => {
const testSettings = async () => {
setIsTesting(true);
let toastId: string | undefined;
try {
await axios.post('/api/v1/settings/notifications/slack', {
enabled: values.enabled,
addToast(
intl.formatMessage(messages.toastSlackTestSending),
{
autoDismiss: false,
appearance: 'info',
},
(id) => {
toastId = id;
}
);
await axios.post('/api/v1/settings/notifications/slack/test', {
enabled: true,
types: values.types,
options: {
webhookUrl: values.webhookUrl,
},
});
addToast(intl.formatMessage(messages.slacksettingssaved), {
appearance: 'success',
if (toastId) {
removeToast(toastId);
}
addToast(intl.formatMessage(messages.toastSlackTestSuccess), {
autoDismiss: true,
appearance: 'success',
});
} catch (e) {
addToast(intl.formatMessage(messages.slacksettingsfailed), {
appearance: 'error',
if (toastId) {
removeToast(toastId);
}
addToast(intl.formatMessage(messages.toastSlackTestFailed), {
autoDismiss: true,
appearance: 'error',
});
} finally {
revalidate();
setIsTesting(false);
}
}}
>
{({
errors,
touched,
isSubmitting,
values,
isValid,
setFieldValue,
}) => {
const testSettings = async () => {
setIsTesting(true);
let toastId: string | undefined;
try {
addToast(
intl.formatMessage(messages.toastSlackTestSending),
{
autoDismiss: false,
appearance: 'info',
},
(id) => {
toastId = id;
}
);
await axios.post('/api/v1/settings/notifications/slack/test', {
enabled: true,
types: values.types,
options: {
webhookUrl: values.webhookUrl,
},
});
};
if (toastId) {
removeToast(toastId);
}
addToast(intl.formatMessage(messages.toastSlackTestSuccess), {
autoDismiss: true,
appearance: 'success',
});
} catch (e) {
if (toastId) {
removeToast(toastId);
}
addToast(intl.formatMessage(messages.toastSlackTestFailed), {
autoDismiss: true,
appearance: 'error',
});
} finally {
setIsTesting(false);
}
};
return (
<Form className="section">
<div className="form-row">
<label htmlFor="isDefault" className="checkbox-label">
{intl.formatMessage(messages.agentenabled)}
</label>
<div className="form-input">
<Field type="checkbox" id="enabled" name="enabled" />
</div>
return (
<Form className="section">
<div className="form-row">
<label htmlFor="isDefault" className="checkbox-label">
{intl.formatMessage(messages.agentenabled)}
<span className="label-required">*</span>
</label>
<div className="form-input">
<Field type="checkbox" id="enabled" name="enabled" />
</div>
<div className="form-row">
<label htmlFor="name" className="text-label">
{intl.formatMessage(messages.webhookUrl)}
<span className="label-required">*</span>
</label>
<div className="form-input">
<div className="form-input-field">
<Field id="webhookUrl" name="webhookUrl" type="text" />
</div>
{errors.webhookUrl && touched.webhookUrl && (
<div className="error">{errors.webhookUrl}</div>
)}
</div>
<div className="form-row">
<label htmlFor="name" className="text-label">
{intl.formatMessage(messages.webhookUrl)}
<span className="label-required">*</span>
<span className="label-tip">
{intl.formatMessage(messages.webhookUrlTip, {
WebhookLink: function WebhookLink(msg) {
return (
<a
href="https://my.slack.com/services/new/incoming-webhook/"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
})}
</span>
</label>
<div className="form-input">
<div className="form-input-field">
<Field
id="webhookUrl"
name="webhookUrl"
type="text"
inputMode="url"
/>
</div>
{errors.webhookUrl && touched.webhookUrl && (
<div className="error">{errors.webhookUrl}</div>
)}
</div>
<NotificationTypeSelector
currentTypes={values.types}
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
/>
<div className="actions">
<div className="flex justify-end">
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="warning"
disabled={isSubmitting || !isValid || isTesting}
onClick={(e) => {
e.preventDefault();
testSettings();
}}
>
{isTesting
? intl.formatMessage(globalMessages.testing)
: intl.formatMessage(globalMessages.test)}
</Button>
</span>
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="primary"
type="submit"
disabled={isSubmitting || !isValid || isTesting}
>
{isSubmitting
? intl.formatMessage(globalMessages.saving)
: intl.formatMessage(globalMessages.save)}
</Button>
</span>
</div>
</div>
<NotificationTypeSelector
currentTypes={values.types}
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
/>
<div className="actions">
<div className="flex justify-end">
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="warning"
disabled={isSubmitting || !isValid || isTesting}
onClick={(e) => {
e.preventDefault();
testSettings();
}}
>
{isTesting
? intl.formatMessage(globalMessages.testing)
: intl.formatMessage(globalMessages.test)}
</Button>
</span>
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="primary"
type="submit"
disabled={isSubmitting || !isValid || isTesting}
>
{isSubmitting
? intl.formatMessage(globalMessages.saving)
: intl.formatMessage(globalMessages.save)}
</Button>
</span>
</div>
</Form>
);
}}
</Formik>
</>
</div>
</Form>
);
}}
</Formik>
);
};

View File

@@ -6,7 +6,6 @@ import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import globalMessages from '../../../i18n/globalMessages';
import Alert from '../../Common/Alert';
import Button from '../../Common/Button';
import LoadingSpinner from '../../Common/LoadingSpinner';
import SensitiveInput from '../../Common/SensitiveInput';
@@ -16,18 +15,20 @@ const messages = defineMessages({
agentenabled: 'Enable Agent',
botUsername: 'Bot Username',
botUsernameTip:
'Allow users to start a chat with the bot and configure their own personal notifications',
botAPI: 'Bot Authentication Token',
'Allow users to also start a chat with your bot and configure their own notifications',
botAPI: 'Bot Authorization Token',
botApiTip:
'<CreateBotLink>Create a bot</CreateBotLink> for use with Overseerr',
chatId: 'Chat ID',
validationBotAPIRequired: 'You must provide a bot authentication token',
chatIdTip:
'Start a chat with your bot, add <GetIdBotLink>@get_id_bot</GetIdBotLink>, and issue the <code>/my_id</code> command',
validationBotAPIRequired: 'You must provide a bot authorization token',
validationChatIdRequired: 'You must provide a valid chat ID',
telegramsettingssaved: 'Telegram notification settings saved successfully!',
telegramsettingsfailed: 'Telegram notification settings failed to save.',
toastTelegramTestSending: 'Sending Telegram test notification…',
toastTelegramTestSuccess: 'Telegram test notification sent!',
toastTelegramTestFailed: 'Telegram test notification failed to send.',
settinguptelegramDescription:
'To configure Telegram notifications, you will need to <CreateBotLink>create a bot</CreateBotLink> and get the bot API key. Additionally, you will need the chat ID for the chat to which you would like to send notifications. You can find this by adding <GetIdBotLink>@get_id_bot</GetIdBotLink> to the chat and issuing the <code>/my_id</code> command.',
sendSilently: 'Send Silently',
sendSilentlyTip: 'Send notifications with no sound',
});
@@ -151,158 +152,141 @@ const NotificationsTelegram: React.FC = () => {
};
return (
<>
<Alert
title={intl.formatMessage(messages.settinguptelegramDescription, {
CreateBotLink: function CreateBotLink(msg) {
return (
<a
href="https://core.telegram.org/bots#6-botfather"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
GetIdBotLink: function GetIdBotLink(msg) {
return (
<a
href="https://telegram.me/get_id_bot"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
code: function code(msg) {
return <code className="bg-opacity-50">{msg}</code>;
},
})}
type="info"
/>
<Form className="section">
<div className="form-row">
<label htmlFor="enabled" className="checkbox-label">
{intl.formatMessage(messages.agentenabled)}
</label>
<div className="form-input">
<Field type="checkbox" id="enabled" name="enabled" />
</div>
<Form className="section">
<div className="form-row">
<label htmlFor="enabled" className="checkbox-label">
{intl.formatMessage(messages.agentenabled)}
<span className="label-required">*</span>
</label>
<div className="form-input">
<Field type="checkbox" id="enabled" name="enabled" />
</div>
<div className="form-row">
<label htmlFor="botUsername" className="text-label">
{intl.formatMessage(messages.botUsername)}
<span className="label-tip">
{intl.formatMessage(messages.botUsernameTip)}
</span>
</label>
<div className="form-input">
<div className="form-input-field">
<Field
id="botUsername"
name="botUsername"
type="text"
placeholder={intl.formatMessage(messages.botUsername)}
/>
</div>
{errors.botUsername && touched.botUsername && (
<div className="error">{errors.botUsername}</div>
)}
</div>
</div>
<div className="form-row">
<label htmlFor="botAPI" className="text-label">
{intl.formatMessage(messages.botAPI)}
<span className="label-required">*</span>
</label>
<div className="form-input">
<div className="form-input-field">
<SensitiveInput
as="field"
id="botAPI"
name="botAPI"
type="text"
placeholder={intl.formatMessage(messages.botAPI)}
/>
</div>
{errors.botAPI && touched.botAPI && (
<div className="error">{errors.botAPI}</div>
)}
</div>
</div>
<div className="form-row">
<label htmlFor="chatId" className="text-label">
{intl.formatMessage(messages.chatId)}
<span className="label-required">*</span>
</label>
<div className="form-input">
<div className="form-input-field">
<Field
id="chatId"
name="chatId"
type="text"
placeholder={intl.formatMessage(messages.chatId)}
/>
</div>
{errors.chatId && touched.chatId && (
<div className="error">{errors.chatId}</div>
)}
</div>
</div>
<div className="form-row">
<label htmlFor="sendSilently" className="checkbox-label">
<span>{intl.formatMessage(messages.sendSilently)}</span>
<span className="label-tip">
{intl.formatMessage(messages.sendSilentlyTip)}
</span>
</label>
<div className="form-input">
<Field
type="checkbox"
id="sendSilently"
name="sendSilently"
</div>
<div className="form-row">
<label htmlFor="botAPI" className="text-label">
{intl.formatMessage(messages.botAPI)}
<span className="label-required">*</span>
<span className="label-tip">
{intl.formatMessage(messages.botApiTip, {
CreateBotLink: function CreateBotLink(msg) {
return (
<a
href="https://core.telegram.org/bots#6-botfather"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
GetIdBotLink: function GetIdBotLink(msg) {
return (
<a
href="https://telegram.me/get_id_bot"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
code: function code(msg) {
return <code className="bg-opacity-50">{msg}</code>;
},
})}
</span>
</label>
<div className="form-input">
<div className="form-input-field">
<SensitiveInput
as="field"
id="botAPI"
name="botAPI"
autoComplete="one-time-code"
/>
</div>
{errors.botAPI && touched.botAPI && (
<div className="error">{errors.botAPI}</div>
)}
</div>
<NotificationTypeSelector
currentTypes={values.types}
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
/>
<div className="actions">
<div className="flex justify-end">
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="warning"
disabled={isSubmitting || !isValid || isTesting}
onClick={(e) => {
e.preventDefault();
testSettings();
}}
>
{isTesting
? intl.formatMessage(globalMessages.testing)
: intl.formatMessage(globalMessages.test)}
</Button>
</span>
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="primary"
type="submit"
disabled={isSubmitting || !isValid || isTesting}
>
{isSubmitting
? intl.formatMessage(globalMessages.saving)
: intl.formatMessage(globalMessages.save)}
</Button>
</span>
</div>
<div className="form-row">
<label htmlFor="botUsername" className="text-label">
{intl.formatMessage(messages.botUsername)}
<span className="label-tip">
{intl.formatMessage(messages.botUsernameTip)}
</span>
</label>
<div className="form-input">
<div className="form-input-field">
<Field id="botUsername" name="botUsername" type="text" />
</div>
{errors.botUsername && touched.botUsername && (
<div className="error">{errors.botUsername}</div>
)}
</div>
</Form>
</>
</div>
<div className="form-row">
<label htmlFor="chatId" className="text-label">
{intl.formatMessage(messages.chatId)}
<span className="label-required">*</span>
</label>
<div className="form-input">
<div className="form-input-field">
<Field id="chatId" name="chatId" type="text" />
</div>
{errors.chatId && touched.chatId && (
<div className="error">{errors.chatId}</div>
)}
</div>
</div>
<div className="form-row">
<label htmlFor="sendSilently" className="checkbox-label">
<span>{intl.formatMessage(messages.sendSilently)}</span>
<span className="label-tip">
{intl.formatMessage(messages.sendSilentlyTip)}
</span>
</label>
<div className="form-input">
<Field type="checkbox" id="sendSilently" name="sendSilently" />
</div>
</div>
<NotificationTypeSelector
currentTypes={values.types}
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
/>
<div className="actions">
<div className="flex justify-end">
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="warning"
disabled={isSubmitting || !isValid || isTesting}
onClick={(e) => {
e.preventDefault();
testSettings();
}}
>
{isTesting
? intl.formatMessage(globalMessages.testing)
: intl.formatMessage(globalMessages.test)}
</Button>
</span>
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="primary"
type="submit"
disabled={isSubmitting || !isValid || isTesting}
>
{isSubmitting
? intl.formatMessage(globalMessages.saving)
: intl.formatMessage(globalMessages.save)}
</Button>
</span>
</div>
</div>
</Form>
);
}}
</Formik>

View File

@@ -105,6 +105,7 @@ const NotificationsWebPush: React.FC = () => {
<div className="form-row">
<label htmlFor="enabled" className="checkbox-label">
{intl.formatMessage(messages.agentenabled)}
<span className="label-required">*</span>
</label>
<div className="form-input">
<Field type="checkbox" id="enabled" name="enabled" />

View File

@@ -209,6 +209,7 @@ const NotificationsWebhook: React.FC = () => {
<div className="form-row">
<label htmlFor="enabled" className="checkbox-label">
{intl.formatMessage(messages.agentenabled)}
<span className="label-required">*</span>
</label>
<div className="form-input">
<Field type="checkbox" id="enabled" name="enabled" />
@@ -221,7 +222,12 @@ const NotificationsWebhook: React.FC = () => {
</label>
<div className="form-input">
<div className="form-input-field">
<Field id="webhookUrl" name="webhookUrl" type="text" />
<Field
id="webhookUrl"
name="webhookUrl"
type="text"
inputMode="url"
/>
</div>
{errors.webhookUrl && touched.webhookUrl && (
<div className="error">{errors.webhookUrl}</div>