diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/form_deserializer.test.tsx b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/form_deserializer.test.tsx index ad5e047d37771..f44f88e368f63 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/form_deserializer.test.tsx +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/form_deserializer.test.tsx @@ -46,7 +46,7 @@ describe('deserializer', () => { ...data, config: { ...data.config, - allowedChannels: ['#general', '#random'], + allowedChannels: [{ name: '#general' }, { name: '#random' }], }, }); }); diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/form_deserializer.ts b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/form_deserializer.ts index 5226204eb6dff..8038589bd2e3a 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/form_deserializer.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/form_deserializer.ts @@ -14,10 +14,10 @@ export const deserializer = (data: ConnectorFormSchema): InternalConnectorForm = const formattedChannels = allowedChannels.map((channel) => { if (channel.name.startsWith('#')) { - return channel.name; + return channel; } - return `#${channel.name}`; + return { ...channel, name: `#${channel.name}` }; }) ?? []; return { diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/form_serializer.test.tsx b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/form_serializer.test.tsx index acd6107873cb2..08647c22eb21f 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/form_serializer.test.tsx +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/form_serializer.test.tsx @@ -35,7 +35,7 @@ describe('serializer', () => { isDeprecated: false, isSystemAction: false, actionTypeId: CONNECTOR_ID, - config: { allowedChannels: ['#general', '#random'] }, + config: { allowedChannels: [{ name: 'general' }, { name: '#random' }] }, secrets: {}, }; diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/form_serializer.ts b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/form_serializer.ts index 7467b79116f42..d2031aa1e1129 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/form_serializer.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/form_serializer.ts @@ -6,10 +6,20 @@ */ import type { ConnectorFormSchema, InternalConnectorForm } from '@kbn/alerts-ui-shared'; +import type { SlackApiConfig } from '@kbn/connector-schemas/slack_api'; export const serializer = (data: InternalConnectorForm): ConnectorFormSchema => { - const formAllowedChannels = (data.config?.allowedChannels as string[]) ?? []; - const allowedChannels = formAllowedChannels.map((option) => ({ name: option })) ?? []; + const formAllowedChannels = + (data.config?.allowedChannels as SlackApiConfig['allowedChannels']) ?? []; + + const allowedChannels = + formAllowedChannels.map((channel) => { + if (channel.name.startsWith('#')) { + return channel; + } + + return { ...channel, name: `#${channel.name}` }; + }) ?? []; return { ...data, diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_connectors.test.tsx b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_connectors.test.tsx index 709c4126435d3..b9942adc0a18f 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_connectors.test.tsx +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_connectors.test.tsx @@ -212,7 +212,7 @@ describe('SlackActionFields renders', () => { expect(screen.getByText('#test')).toBeInTheDocument(); }); - it('changes the values correctly', async () => { + it('changes the values correctly and preserve the id of the channel', async () => { render( { await userEvent.click(screen.getByTestId('secrets.token-input')); await userEvent.paste('token updated'); - await userEvent.click(screen.getByTestId('comboBoxClearButton')); await userEvent.click(screen.getByTestId('comboBoxSearchInput')); await userEvent.type(screen.getByTestId('comboBoxSearchInput'), '#new-channel{enter}'); @@ -247,7 +246,7 @@ describe('SlackActionFields renders', () => { token: 'token updated', }, config: { - allowedChannels: [{ name: '#new-channel' }], + allowedChannels: [{ id: 'channel-id', name: '#test' }, { name: '#new-channel' }], }, }, isValid: true, diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_connectors.tsx b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_connectors.tsx index 87018e0d23680..5f62b8d789d1f 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_connectors.tsx +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_connectors.tsx @@ -5,19 +5,32 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback } from 'react'; import type { ActionConnectorFieldsProps, ConfigFieldSchema, SecretsFieldSchema, } from '@kbn/triggers-actions-ui-plugin/public'; import { SimpleConnectorForm, useKibana } from '@kbn/triggers-actions-ui-plugin/public'; +import type { EuiComboBoxOptionOption } from '@elastic/eui'; import { EuiLink, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import type { DocLinksStart } from '@kbn/core/public'; +import type { FieldValidateResponse } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { + useFormContext, + useFormData, + VALIDATION_TYPES, +} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import type { SlackApiConfig } from '@kbn/connector-schemas/slack_api'; import * as i18n from './translations'; +interface AllowedChannels { + id: string; + name: string; +} + const getSecretsFormSchema = (docLinks: DocLinksStart): SecretsFieldSchema[] => [ { id: 'token', @@ -34,59 +47,113 @@ const getSecretsFormSchema = (docLinks: DocLinksStart): SecretsFieldSchema[] => }, ]; -const getConfigFormSchema = (): ConfigFieldSchema[] => [ - { - id: 'allowedChannels', - isRequired: false, - label: i18n.ALLOWED_CHANNELS, - type: 'COMBO_BOX', - validations: [ - { - isBlocking: true, - validator: (args) => { - const valueAsArray = Array.isArray(args.value) ? args.value : [args.value]; - - if (valueAsArray.length === 0) { - return; - } - - const areAllValid = valueAsArray.every((value) => value.startsWith('#')); - - if (areAllValid) { - return; - } - - return { - code: 'ERR_FIELD_FORMAT', - formatType: 'COMBO_BOX', - message: i18n.CHANNEL_NAME_ERROR, - }; - }, +const areChannelsValid = (channels: AllowedChannels[]) => { + if (channels.length === 0) { + return true; + } + + const areAllValid = channels.every((channel) => channel.name.startsWith('#')); + + if (areAllValid) { + return true; + } + + return false; +}; + +const getAllowedChannlesConfigSchema = ( + euiFieldProps: ConfigFieldSchema['euiFieldProps'] +): ConfigFieldSchema => ({ + id: 'allowedChannels', + isRequired: false, + label: i18n.ALLOWED_CHANNELS, + type: 'COMBO_BOX', + validations: [ + { + isBlocking: true, + validator: (args) => { + const isValid = areChannelsValid(args.value); + + if (isValid) { + return; + } + + return { + code: 'ERR_FIELD_FORMAT', + formatType: 'COMBO_BOX', + message: i18n.CHANNEL_NAME_ERROR, + }; }, - ], - euiFieldProps: { - noSuggestions: true, - autoComplete: 'off', - append: ( - - {i18n.OPTIONAL_LABEL} - - ), }, + ], + euiFieldProps: { + ...euiFieldProps, + noSuggestions: true, + autoComplete: 'off', + append: ( + + {i18n.OPTIONAL_LABEL} + + ), }, -]; +}); export const SlackActionFieldsComponents: React.FC = ({ readOnly, isEdit, }) => { const { docLinks } = useKibana().services; + const { getFields } = useFormContext(); + + const [{ config }] = useFormData({ + watch: ['config.allowedChannels'], + }); + + const { allowedChannels = [] }: SlackApiConfig = config ?? {}; + + const selectedOptions = allowedChannels.map((channel) => ({ + label: channel.name, + value: channel, + })); + + const fields = getFields(); + + const allowedChannelsField = fields['config.allowedChannels']; + + const onCreateOption = useCallback( + (value: string) => { + const finalValue = { name: value }; + + const { isValid } = allowedChannelsField.validate({ + value: [finalValue], + validationType: VALIDATION_TYPES.ARRAY_ITEM, + }) as FieldValidateResponse; + + if (!isValid) { + return false; + } + + allowedChannelsField.setValue([...allowedChannels, finalValue]); + }, + [allowedChannels, allowedChannelsField] + ); + + const onChange = useCallback( + (options: EuiComboBoxOptionOption[]) => { + allowedChannelsField.setValue( + options.map((option) => ({ name: option.value?.name, id: option.value?.id })) + ); + }, + [allowedChannelsField] + ); return ( ); diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/components/simple_connector_form.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/components/simple_connector_form.tsx index 89c228021fbf3..607f2e7e03791 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/components/simple_connector_form.tsx +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/components/simple_connector_form.tsx @@ -18,9 +18,9 @@ import { FIELD_TYPES, getUseField } from '@kbn/es-ui-shared-plugin/static/forms/ import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; import { i18n } from '@kbn/i18n'; -type Validations = Array>; +type Validations = Array>; -export interface CommonFieldSchema { +export interface CommonFieldSchema { id: string; label: string; helpText?: string | ReactNode; @@ -30,7 +30,7 @@ export interface CommonFieldSchema { validations?: Validations; } -export interface ConfigFieldSchema extends CommonFieldSchema { +export interface ConfigFieldSchema extends CommonFieldSchema { isUrlField?: boolean; requireTld?: boolean; defaultValue?: T; @@ -53,7 +53,7 @@ const UseTextField = getUseField({ component: Field }); const UseComboBoxField = getUseField({ component: ComboBoxField }); const { emptyField, urlField } = fieldValidators; -const getFieldConfig = ({ +const getFieldConfig = ({ label, isRequired = true, isUrlField = false,