diff --git a/public/app/features/alerting/unified/components/receivers/__snapshots__/NewReceiverView.test.tsx.snap b/public/app/features/alerting/unified/components/receivers/__snapshots__/NewReceiverView.test.tsx.snap
index f4c781df037..15473192d14 100644
--- a/public/app/features/alerting/unified/components/receivers/__snapshots__/NewReceiverView.test.tsx.snap
+++ b/public/app/features/alerting/unified/components/receivers/__snapshots__/NewReceiverView.test.tsx.snap
@@ -17,7 +17,7 @@ exports[`new receiver should be able to test and save a receiver 1`] = `
{
"disableResolveMessage": false,
"name": "test",
- "secureSettings": {},
+ "secureFields": {},
"settings": {
"addresses": "tester@grafana.com",
"singleEmail": false,
diff --git a/public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx b/public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx
index 1c99524f434..fc8d4cc73f1 100644
--- a/public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx
+++ b/public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx
@@ -2,22 +2,31 @@ import * as React from 'react';
import { DeepMap, FieldError, FieldErrors, useFormContext } from 'react-hook-form';
import { Field, SecretInput } from '@grafana/ui';
-import { NotificationChannelOption, NotificationChannelSecureFields } from 'app/types';
+import { NotificationChannelOption, NotificationChannelSecureFields, OptionMeta } from 'app/types';
-import { ChannelValues, ReceiverFormValues } from '../../../types/receiver-form';
+import {
+ ChannelValues,
+ CloudChannelValues,
+ GrafanaChannelValues,
+ ReceiverFormValues,
+} from '../../../types/receiver-form';
import { OptionField } from './fields/OptionField';
export interface Props
{
defaultValues: R;
selectedChannelOptions: NotificationChannelOption[];
- secureFields: NotificationChannelSecureFields;
onResetSecureField: (key: string) => void;
+ onDeleteSubform?: (settingsPath: string, option: NotificationChannelOption) => void;
errors?: FieldErrors;
- pathPrefix?: string;
+ /**
+ * The path for the integration in the array of integrations.
+ * This is used to access the settings and secure fields for the integration in a type-safe way.
+ */
+ integrationPrefix: `items.${number}`;
+ canEditProtectedFields: boolean;
readOnly?: boolean;
-
customValidators?: Record['customValidator']>;
}
@@ -25,14 +34,24 @@ export function ChannelOptions({
defaultValues,
selectedChannelOptions,
onResetSecureField,
- secureFields,
+ onDeleteSubform,
errors,
- pathPrefix = '',
+ integrationPrefix,
readOnly = false,
customValidators = {},
+ canEditProtectedFields,
}: Props): JSX.Element {
- const { watch } = useFormContext>();
- const currentFormValues = watch(); // react hook form types ARE LYING!
+ const { watch } = useFormContext>();
+
+ const [settings, secureFields] = watch([`${integrationPrefix}.settings`, `${integrationPrefix}.secureFields`]);
+
+ // Note: settingsPath includes a trailing dot for OptionField, unlike the path used in watch()
+ const settingsPath = `${integrationPrefix}.settings.` as const;
+
+ const getOptionMeta = (option: NotificationChannelOption): OptionMeta => ({
+ required: determineRequired(option, settings, secureFields),
+ readOnly: determineReadOnly(option, settings, secureFields, canEditProtectedFields),
+ });
return (
<>
@@ -41,43 +60,97 @@ export function ChannelOptions({
// Some options can be dependent on other options, this determines what is selected in the dependency options
// I think this needs more thought.
// pathPrefix = items.index.
- const paths = pathPrefix.split('.');
- const selectedOptionValue =
- paths.length >= 2 ? currentFormValues.items?.[Number(paths[1])].settings?.[option.showWhen.field] : undefined;
+ // const paths = pathPrefix.split('.');
+ const selectedOptionValue = settings?.[option.showWhen.field];
if (option.showWhen.field && selectedOptionValue !== option.showWhen.is) {
return null;
}
- if (secureFields && secureFields[option.propertyName]) {
+ if (secureFields && secureFields[option.secureFieldKey ?? option.propertyName]) {
return (
-
- onResetSecureField(option.propertyName)} isConfigured />
+
+ onResetSecureField(option.secureFieldKey ?? option.propertyName)}
+ isConfigured
+ />
);
}
- const error: FieldError | DeepMap | undefined = (
- (option.secure ? errors?.secureSettings : errors?.settings) as DeepMap | undefined
- )?.[option.propertyName];
+ const errorSource = option.secure ? errors?.secureFields : errors?.settings;
+ const propertyKey = option.secureFieldKey ?? option.propertyName;
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
+ const error = (
+ errorSource as Record, FieldError>> | undefined
+ )?.[propertyKey];
const defaultValue = defaultValues?.settings?.[option.propertyName];
return (
);
})}
>
);
}
+
+const determineRequired = (
+ option: NotificationChannelOption,
+ settings: Record,
+ secureFields: NotificationChannelSecureFields
+) => {
+ if (!option.required) {
+ return false;
+ }
+
+ if (!option.dependsOn) {
+ return option.required ? 'Required' : false;
+ }
+
+ // TODO: This doesn't work with nested secureFields.
+ const dependentOn = Boolean(settings[option.dependsOn]) || Boolean(secureFields[option.dependsOn]);
+
+ if (dependentOn) {
+ return false;
+ }
+
+ return 'Required';
+};
+
+const determineReadOnly = (
+ option: NotificationChannelOption,
+ settings: Record,
+ secureFields: NotificationChannelSecureFields,
+ canEditProtectedFields: boolean
+) => {
+ if (option.protected && !canEditProtectedFields) {
+ return true;
+ }
+
+ // Handle fields with dependencies (e.g., field B depends on field A being set)
+ if (!option.dependsOn) {
+ return false;
+ }
+
+ // TODO: This doesn't work with nested secureFields.
+ return Boolean(settings[option.dependsOn]) || Boolean(secureFields[option.dependsOn]);
+};
diff --git a/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.test.tsx b/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.test.tsx
new file mode 100644
index 00000000000..e427f98d483
--- /dev/null
+++ b/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.test.tsx
@@ -0,0 +1,249 @@
+import 'core-js/stable/structured-clone';
+import { FormProvider, useForm } from 'react-hook-form';
+import { clickSelectOption } from 'test/helpers/selectOptionInTest';
+import { render } from 'test/test-utils';
+import { byRole, byTestId } from 'testing-library-selector';
+
+import { grafanaAlertNotifiers } from 'app/features/alerting/unified/mockGrafanaNotifiers';
+import { AlertmanagerProvider } from 'app/features/alerting/unified/state/AlertmanagerContext';
+
+import { ChannelSubForm } from './ChannelSubForm';
+import { GrafanaCommonChannelSettings } from './GrafanaCommonChannelSettings';
+import { Notifier } from './notifiers';
+
+type TestChannelValues = {
+ __id: string;
+ type: string;
+ settings: Record;
+ secureFields: Record;
+};
+
+type TestReceiverFormValues = {
+ name: string;
+ items: TestChannelValues[];
+};
+
+const ui = {
+ typeSelector: byTestId('items.0.type'),
+ settings: {
+ webhook: {
+ url: byRole('textbox', { name: /^URL/ }),
+ optionalSettings: byRole('button', { name: /optional webhook settings/i }),
+ title: {
+ container: byTestId('items.0.settings.title'),
+ input: byRole('textbox', { name: /^Title/ }),
+ },
+ message: {
+ container: byTestId('items.0.settings.message'),
+ input: byRole('textbox', { name: /^Message/ }),
+ },
+ },
+ slack: {
+ recipient: byTestId('items.0.settings.recipient'),
+ token: byTestId('items.0.settings.token'),
+ username: byTestId('items.0.settings.username'),
+ webhookUrl: byRole('textbox', { name: /^Webhook URL/ }),
+ },
+ googlechat: {
+ optionalSettings: byRole('button', { name: /optional google hangouts chat settings/i }),
+ url: byRole('textbox', { name: /^URL/ }),
+ title: {
+ input: byRole('textbox', { name: /^Title/ }),
+ container: byTestId('items.0.settings.title'),
+ },
+ message: {
+ input: byRole('textbox', { name: /^Message/ }),
+ container: byTestId('items.0.settings.message'),
+ },
+ },
+ },
+};
+
+const notifiers: Notifier[] = [
+ { dto: grafanaAlertNotifiers.webhook, meta: { enabled: true, order: 1 } },
+ { dto: grafanaAlertNotifiers.slack, meta: { enabled: true, order: 2 } },
+ { dto: grafanaAlertNotifiers.googlechat, meta: { enabled: true, order: 3 } },
+ { dto: grafanaAlertNotifiers.sns, meta: { enabled: true, order: 4 } },
+ { dto: grafanaAlertNotifiers.oncall, meta: { enabled: true, order: 5 } },
+];
+
+describe('ChannelSubForm', () => {
+ function TestFormWrapper({ defaults, initial }: { defaults: TestChannelValues; initial?: TestChannelValues }) {
+ const form = useForm({
+ defaultValues: {
+ name: 'test-contact-point',
+ items: [defaults],
+ },
+ });
+
+ return (
+
+
+
+
+
+ );
+ }
+
+ function renderForm(defaults: TestChannelValues, initial?: TestChannelValues) {
+ return render();
+ }
+
+ it('switching type hides prior fields and shows new ones', async () => {
+ renderForm({
+ __id: 'id-0',
+ type: 'webhook',
+ settings: { url: '' },
+ secureFields: {},
+ });
+
+ expect(ui.typeSelector.get()).toHaveTextContent('Webhook');
+
+ expect(ui.settings.webhook.url.get()).toBeInTheDocument();
+
+ expect(ui.settings.slack.recipient.query()).not.toBeInTheDocument();
+
+ await clickSelectOption(ui.typeSelector.get(), 'Slack');
+ expect(ui.typeSelector.get()).toHaveTextContent('Slack');
+
+ expect(ui.settings.slack.recipient.get()).toBeInTheDocument();
+ expect(ui.settings.slack.token.get()).toBeInTheDocument();
+ expect(ui.settings.slack.username.get()).toBeInTheDocument();
+ });
+
+ it('should clear secure fields when switching integration types', async () => {
+ const googlechatDefaults: TestChannelValues = {
+ __id: 'id-0',
+ type: 'googlechat',
+ settings: { title: 'Alert Title', message: 'Alert Message' },
+ secureFields: { url: true },
+ };
+
+ const { user } = renderForm(googlechatDefaults, googlechatDefaults);
+
+ expect(ui.typeSelector.get()).toHaveTextContent('Google Hangouts Chat');
+
+ expect(ui.settings.googlechat.url.get()).toBeDisabled();
+ expect(ui.settings.googlechat.url.get()).toHaveValue('configured');
+
+ await user.click(ui.settings.googlechat.optionalSettings.get());
+
+ expect(ui.settings.googlechat.title.input.get()).toHaveValue('Alert Title');
+ expect(ui.settings.googlechat.message.input.get()).toHaveValue('Alert Message');
+
+ await clickSelectOption(ui.typeSelector.get(), 'Webhook');
+ expect(ui.typeSelector.get()).toHaveTextContent('Webhook');
+
+ // Webhook URL field should now be present and empty (settings cleared)
+ expect(ui.settings.webhook.url.get()).toHaveValue('');
+ expect(ui.settings.webhook.title.container.get()).toBeInTheDocument();
+ expect(ui.settings.webhook.message.container.get()).toBeInTheDocument();
+
+ // If value for templated fields is empty the input should not be present
+ expect(ui.settings.webhook.message.input.query()).not.toBeInTheDocument();
+ expect(ui.settings.webhook.title.input.query()).not.toBeInTheDocument();
+ });
+
+ it('should clear settings when switching from webhook to googlechat', async () => {
+ const webhookDefaults: TestChannelValues = {
+ __id: 'id-0',
+ type: 'webhook',
+ settings: { url: 'https://example.com/webhook', title: 'Webhook Title', message: 'Webhook Message' },
+ secureFields: {},
+ };
+
+ const { user } = renderForm(webhookDefaults, webhookDefaults);
+
+ expect(ui.typeSelector.get()).toHaveTextContent('Webhook');
+
+ expect(ui.settings.webhook.url.get()).toHaveValue('https://example.com/webhook');
+
+ await user.click(ui.settings.webhook.optionalSettings.get());
+ expect(ui.settings.webhook.title.input.get()).toHaveValue('Webhook Title');
+ expect(ui.settings.webhook.message.input.get()).toHaveValue('Webhook Message');
+
+ await clickSelectOption(ui.typeSelector.get(), 'Google Hangouts Chat');
+ expect(ui.typeSelector.get()).toHaveTextContent('Google Hangouts Chat');
+
+ // Google Chat URL field should now be present and empty (settings cleared)
+ expect(ui.settings.googlechat.url.get()).toHaveValue('');
+ expect(ui.settings.googlechat.title.container.get()).toBeInTheDocument();
+ expect(ui.settings.googlechat.message.container.get()).toBeInTheDocument();
+
+ // If value for templated fields is empty the input should not be present
+ expect(ui.settings.googlechat.message.input.query()).not.toBeInTheDocument();
+ expect(ui.settings.googlechat.title.input.query()).not.toBeInTheDocument();
+ });
+
+ it('should restore initial values when switching back to original type', async () => {
+ const googlechatDefaults: TestChannelValues = {
+ __id: 'id-0',
+ type: 'googlechat',
+ settings: { title: 'Original Title', message: 'Original Message' },
+ secureFields: { url: true },
+ };
+
+ const { user } = renderForm(googlechatDefaults, googlechatDefaults);
+
+ expect(ui.typeSelector.get()).toHaveTextContent('Google Hangouts Chat');
+
+ expect(ui.settings.googlechat.url.get()).toBeDisabled();
+ expect(ui.settings.googlechat.url.get()).toHaveValue('configured');
+
+ await user.click(ui.settings.googlechat.optionalSettings.get());
+
+ expect(ui.settings.googlechat.title.input.get()).toHaveValue('Original Title');
+ expect(ui.settings.googlechat.message.input.get()).toHaveValue('Original Message');
+
+ // Switch to a different type
+ await clickSelectOption(ui.typeSelector.get(), 'Webhook');
+ expect(ui.typeSelector.get()).toHaveTextContent('Webhook');
+ expect(ui.settings.webhook.url.get()).toHaveValue('');
+
+ // Switch back to the original type
+ await clickSelectOption(ui.typeSelector.get(), 'Google Hangouts Chat');
+ expect(ui.typeSelector.get()).toHaveTextContent('Google Hangouts Chat');
+
+ // Original settings and secure fields should be restored
+ expect(ui.settings.googlechat.url.get()).toBeDisabled();
+ expect(ui.settings.googlechat.url.get()).toHaveValue('configured');
+
+ expect(ui.settings.googlechat.title.input.get()).toHaveValue('Original Title');
+ expect(ui.settings.googlechat.message.input.get()).toHaveValue('Original Message');
+ });
+
+ it('should maintain secure field isolation across multiple type switches', async () => {
+ const googlechatDefaults: TestChannelValues = {
+ __id: 'id-0',
+ type: 'googlechat',
+ settings: {},
+ secureFields: { url: true },
+ };
+
+ renderForm(googlechatDefaults, googlechatDefaults);
+
+ expect(ui.typeSelector.get()).toHaveTextContent('Google Hangouts Chat');
+ expect(ui.settings.googlechat.url.get()).toBeDisabled();
+ expect(ui.settings.googlechat.url.get()).toHaveValue('configured');
+
+ // Switch to Slack
+ await clickSelectOption(ui.typeSelector.get(), 'Slack');
+ expect(ui.typeSelector.get()).toHaveTextContent('Slack');
+
+ // Slack should not have any secure fields from Google Chat
+ const slackUrl = ui.settings.slack.webhookUrl.get();
+ expect(slackUrl).toBeEnabled();
+ expect(slackUrl).toHaveValue('');
+ });
+});
diff --git a/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx b/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx
index ff6b52d051d..758ef67f6c4 100644
--- a/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx
+++ b/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx
@@ -1,35 +1,41 @@
import { css } from '@emotion/css';
import { sortBy } from 'lodash';
import * as React from 'react';
-import { useCallback, useEffect, useMemo, useState } from 'react';
-import { Controller, FieldErrors, FieldValues, useFormContext } from 'react-hook-form';
+import { useEffect, useMemo } from 'react';
+import { Controller, FieldErrors, useFormContext } from 'react-hook-form';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { Alert, Button, Field, Select, Stack, Text, useStyles2 } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
+import { NotificationChannelOption } from 'app/types';
-import { useUnifiedAlertingSelector } from '../../../hooks/useUnifiedAlertingSelector';
-import { ChannelValues, CommonSettingsComponentType } from '../../../types/receiver-form';
+import {
+ ChannelValues,
+ CloudChannelValues,
+ CommonSettingsComponentType,
+ GrafanaChannelValues,
+ ReceiverFormValues,
+} from '../../../types/receiver-form';
import { OnCallIntegrationType } from '../grafanaAppReceivers/onCall/useOnCallIntegration';
import { ChannelOptions } from './ChannelOptions';
import { CollapsibleSection } from './CollapsibleSection';
import { Notifier } from './notifiers';
-interface Props {
+interface Props {
defaultValues: R;
initialValues?: R;
- pathPrefix: string;
+ pathPrefix: `items.${number}.`;
+ integrationIndex: number;
notifiers: Notifier[];
onDuplicate: () => void;
onTest?: () => void;
commonSettingsComponent: CommonSettingsComponentType;
-
- secureFields?: Record;
errors?: FieldErrors;
onDelete?: () => void;
isEditable?: boolean;
isTestable?: boolean;
+ canEditProtectedFields: boolean;
customValidators?: React.ComponentProps['customValidators'];
}
@@ -38,74 +44,130 @@ export function ChannelSubForm({
defaultValues,
initialValues,
pathPrefix,
+ integrationIndex,
onDuplicate,
onDelete,
onTest,
notifiers,
errors,
- secureFields,
commonSettingsComponent: CommonSettingsComponent,
isEditable = true,
isTestable,
+ canEditProtectedFields,
customValidators = {},
}: Props): JSX.Element {
const styles = useStyles2(getStyles);
+ const { control, watch, register, trigger, formState, setValue, getValues } =
+ useFormContext>();
- const fieldName = useCallback((fieldName: string) => `${pathPrefix}${fieldName}`, [pathPrefix]);
+ const channelFieldPath = `items.${integrationIndex}` as const;
+ const typeFieldPath = `${channelFieldPath}.type` as const;
+ const settingsFieldPath = `${channelFieldPath}.settings` as const;
+ const secureFieldsPath = `${channelFieldPath}.secureFields` as const;
- const { control, watch, register, trigger, formState, setValue } = useFormContext();
- const selectedType = watch(fieldName('type')) ?? defaultValues.type; // nope, setting "default" does not work at all.
- const parse_mode = watch(fieldName('settings.parse_mode'));
- const { loading: testingReceiver } = useUnifiedAlertingSelector((state) => state.testReceivers);
+ const selectedType = watch(typeFieldPath) ?? defaultValues.type;
+ const parse_mode = watch(`${settingsFieldPath}.parse_mode`);
// TODO I don't like integration specific code here but other ways require a bigger refactoring
- const onCallIntegrationType = watch(fieldName('settings.integration_type'));
+ const onCallIntegrationType = watch(`${settingsFieldPath}.integration_type`);
const isTestAvailable = onCallIntegrationType !== OnCallIntegrationType.NewIntegration;
useEffect(() => {
- register(`${pathPrefix}.__id`);
+ register(`${channelFieldPath}.__id`);
/* Need to manually register secureFields or else they'll
be lost when testing a contact point */
- register(`${pathPrefix}.secureFields`);
- }, [register, pathPrefix]);
+ register(`${channelFieldPath}.secureFields`);
+ }, [register, channelFieldPath]);
// Prevent forgetting about initial values when switching the integration type and the oncall integration type
useEffect(() => {
// Restore values when switching back from a changed integration to the default one
- const subscription = watch((v, { name, type }) => {
- const value = name ? v[name] : '';
- if (initialValues && name === fieldName('type') && value === initialValues.type && type === 'change') {
- setValue(fieldName('settings'), initialValues.settings);
+ const subscription = watch((formValues, { name, type }) => {
+ // @ts-expect-error name is valid key for formValues
+ const value = name ? getValues(name, formValues) : '';
+ if (initialValues && name === typeFieldPath && value === initialValues.type && type === 'change') {
+ setValue(settingsFieldPath, initialValues.settings);
+ setValue(secureFieldsPath, initialValues.secureFields);
+ } else if (name === typeFieldPath && type === 'change') {
+ // When switching to a new notifier, set the default settings to remove all existing settings
+ // from the previous notifier
+ const newNotifier = notifiers.find(({ dto: { type } }) => type === value);
+ const defaultNotifierSettings = newNotifier ? getDefaultNotifierSettings(newNotifier) : {};
+
+ // Not sure why, but verriding settingsFieldPath is not enough if notifiers have the same settings fields, like url, title
+ const currentSettings = getValues(settingsFieldPath) ?? {};
+ Object.keys(currentSettings).forEach((key) => {
+ if (!defaultNotifierSettings[key]) {
+ setValue(`${settingsFieldPath}.${key}`, defaultNotifierSettings[key]);
+ }
+ });
+
+ setValue(settingsFieldPath, defaultNotifierSettings);
+ setValue(secureFieldsPath, {});
}
+
// Restore initial value of an existing oncall integration
if (
initialValues &&
- name === fieldName('settings.integration_type') &&
+ name === `${settingsFieldPath}.integration_type` &&
value === OnCallIntegrationType.ExistingIntegration
) {
- setValue(fieldName('settings.url'), initialValues.settings.url);
+ setValue(`${settingsFieldPath}.url`, initialValues.settings.url);
}
});
return () => subscription.unsubscribe();
- }, [selectedType, initialValues, setValue, fieldName, watch]);
-
- const [_secureFields, setSecureFields] = useState>(secureFields ?? {});
+ }, [
+ selectedType,
+ initialValues,
+ setValue,
+ settingsFieldPath,
+ typeFieldPath,
+ secureFieldsPath,
+ getValues,
+ watch,
+ defaultValues.settings,
+ defaultValues.secureFields,
+ notifiers,
+ ]);
const onResetSecureField = (key: string) => {
- if (_secureFields[key]) {
- const updatedSecureFields = { ..._secureFields };
- updatedSecureFields[key] = '';
- setSecureFields(updatedSecureFields);
- setValue(`${pathPrefix}.secureFields`, updatedSecureFields);
+ // formSecureFields might not be up to date if this function is called multiple times in a row
+ const currentSecureFields = getValues(`${channelFieldPath}.secureFields`);
+ if (currentSecureFields[key]) {
+ setValue(`${channelFieldPath}.secureFields`, { ...currentSecureFields, [key]: '' });
}
};
+ const findSecureFieldsRecursively = (options: NotificationChannelOption[]): string[] => {
+ const secureFields: string[] = [];
+ options?.forEach((option) => {
+ if (option.secure && option.secureFieldKey) {
+ secureFields.push(option.secureFieldKey);
+ }
+ if (option.subformOptions) {
+ secureFields.push(...findSecureFieldsRecursively(option.subformOptions));
+ }
+ });
+ return secureFields;
+ };
+
+ const onDeleteSubform = (settingsPath: string, option: NotificationChannelOption) => {
+ // Get all subform options with secure=true recursively.
+ const relatedSecureFields = findSecureFieldsRecursively(option.subformOptions ?? []);
+ relatedSecureFields.forEach((key) => {
+ onResetSecureField(key);
+ });
+ const fieldPath = settingsPath.startsWith(`${channelFieldPath}.settings.`)
+ ? settingsPath.slice(`${channelFieldPath}.settings.`.length)
+ : settingsPath;
+ setValue(`${settingsFieldPath}.${fieldPath}`, undefined);
+ };
+
const typeOptions = useMemo(
(): SelectableValue[] =>
- sortBy(notifiers, ({ dto, meta }) => [meta?.order ?? 0, dto.name])
- // .notifiers.sort((a, b) => a.dto.name.localeCompare(b.dto.name))
- .map(({ dto: { name, type }, meta }) => ({
+ sortBy(notifiers, ({ dto, meta }) => [meta?.order ?? 0, dto.name]).map(
+ ({ dto: { name, type }, meta }) => ({
// @ts-expect-error ReactNode is supported
label: (
@@ -116,7 +178,8 @@ export function ChannelSubForm({
value: type,
description: meta?.description,
isDisabled: meta ? !meta.enabled : false,
- })),
+ })
+ ),
[notifiers]
);
@@ -137,8 +200,8 @@ export function ChannelSubForm({
const showTelegramWarning = isTelegram && !isParseModeNone;
// if there are mandatory options defined, optional options will be hidden by a collapse
// if there aren't mandatory options, all options will be shown without collapse
- const mandatoryOptions = notifier?.dto.options.filter((o) => o.required);
- const optionalOptions = notifier?.dto.options.filter((o) => !o.required);
+ const mandatoryOptions = notifier?.dto.options.filter((o) => o.required) ?? [];
+ const optionalOptions = notifier?.dto.options.filter((o) => !o.required) ?? [];
const contactPointTypeInputId = `contact-point-type-${pathPrefix}`;
return (
@@ -151,7 +214,8 @@ export function ChannelSubForm({
data-testid={`${pathPrefix}type`}
>
(