[ResponseOps][Cases] Fix template's custom fields bugs (#187591)

## Summary

Fixes https://github.com/elastic/kibana/issues/187333

## Testing behaviour: 
Issue 1: verify similar behaviour from API as well.

1. Create a template
2. Add new toggle custom field with default value as true
3. Go to create case, See that new toggle custom field has value: true
4. Select recently created template
5. Toggle custom field new custom field with it's default value

Issue 2: verify similar behaviour from API as well.
1. Create a text custom field with default value
2. Create a template
3. Set text custom field value to empty
4. Save template
5. Go to create case
6. Select recently created template
7. See that text custom field value is updated as per template's custom
field value
This commit is contained in:
Janki Salvi 2024-07-11 17:13:19 +01:00 committed by GitHub
parent 0c0ce0d9c5
commit 6b0d628053
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1026 additions and 253 deletions

View file

@ -600,6 +600,11 @@ describe('CommonFlyout ', () => {
type: 'toggle',
value: true,
},
{
key: 'test_key_3',
type: 'text',
value: null,
},
{
key: 'test_key_4',
type: 'toggle',

View file

@ -724,6 +724,166 @@ describe('ConfigureCases', () => {
});
});
it('deletes a custom field from template while deleting custom field from configuration', async () => {
useGetCaseConfigurationMock.mockImplementation(() => ({
...useCaseConfigureResponse,
data: {
...useCaseConfigureResponse.data,
customFields: customFieldsConfigurationMock,
templates: [
{
key: 'test_template_4',
name: 'Fourth test template',
caseFields: {
title: 'Case with sample template 4',
description: 'case desc',
customFields: [
{
key: customFieldsConfigurationMock[0].key,
type: CustomFieldTypes.TEXT,
value: 'this is a text field value',
},
],
},
},
],
},
}));
appMockRender.render(<ConfigureCases />);
const list = await screen.findByTestId('custom-fields-list');
userEvent.click(
within(list).getByTestId(`${customFieldsConfigurationMock[0].key}-custom-field-delete`)
);
expect(await screen.findByTestId('confirm-delete-modal')).toBeInTheDocument();
userEvent.click(screen.getByText('Delete'));
await waitFor(() => {
expect(persistCaseConfigure).toHaveBeenCalledWith({
connector: {
id: 'none',
name: 'none',
type: ConnectorTypes.none,
fields: null,
},
closureType: 'close-by-user',
customFields: [
{ ...customFieldsConfigurationMock[1] },
{ ...customFieldsConfigurationMock[2] },
{ ...customFieldsConfigurationMock[3] },
],
templates: [
{
key: 'test_template_4',
name: 'Fourth test template',
caseFields: {
title: 'Case with sample template 4',
description: 'case desc',
customFields: [],
},
},
],
id: '',
version: '',
});
});
});
it('adds a custom field to template while adding a new custom field', async () => {
useGetCaseConfigurationMock.mockImplementation(() => ({
...useCaseConfigureResponse,
data: {
...useCaseConfigureResponse.data,
customFields: customFieldsConfigurationMock,
templates: [
{
key: 'test_template_4',
name: 'Fourth test template',
caseFields: null,
},
],
},
}));
appMockRender.render(<ConfigureCases />);
userEvent.click(await screen.findByTestId(`add-custom-field`));
expect(await screen.findByTestId('common-flyout')).toBeInTheDocument();
userEvent.paste(screen.getByTestId('custom-field-label-input'), 'New custom field');
userEvent.click(screen.getByTestId('text-custom-field-required'));
userEvent.paste(
screen.getByTestId('text-custom-field-default-value'),
'This is a default value'
);
userEvent.click(screen.getByTestId('common-flyout-save'));
await waitFor(() => {
expect(persistCaseConfigure).toHaveBeenCalledWith({
connector: {
id: 'none',
name: 'none',
type: ConnectorTypes.none,
fields: null,
},
closureType: 'close-by-user',
customFields: [
...customFieldsConfigurationMock,
{
key: expect.anything(),
label: 'New custom field',
type: CustomFieldTypes.TEXT as const,
required: true,
defaultValue: 'This is a default value',
},
],
templates: [
{
key: 'test_template_4',
name: 'Fourth test template',
caseFields: {
customFields: [
{
key: customFieldsConfigurationMock[0].key,
type: customFieldsConfigurationMock[0].type,
value: customFieldsConfigurationMock[0].defaultValue,
},
{
key: customFieldsConfigurationMock[1].key,
type: customFieldsConfigurationMock[1].type,
value: customFieldsConfigurationMock[1].defaultValue,
},
{
key: customFieldsConfigurationMock[2].key,
type: customFieldsConfigurationMock[2].type,
value: null,
},
{
key: customFieldsConfigurationMock[3].key,
type: customFieldsConfigurationMock[3].type,
value: false,
},
{
key: expect.anything(),
type: CustomFieldTypes.TEXT as const,
value: 'This is a default value',
},
],
},
},
],
id: '',
version: '',
});
});
});
it('updates a custom field correctly', async () => {
useGetCaseConfigurationMock.mockImplementation(() => ({
...useCaseConfigureResponse,
@ -929,6 +1089,11 @@ describe('ConfigureCases', () => {
type: customFieldsConfigurationMock[1].type,
value: customFieldsConfigurationMock[1].defaultValue,
},
{
key: customFieldsConfigurationMock[2].key,
type: customFieldsConfigurationMock[2].type,
value: null,
},
{
key: customFieldsConfigurationMock[3].key,
type: customFieldsConfigurationMock[3].type,

View file

@ -24,7 +24,11 @@ import {
import type { ActionConnectorTableItem } from '@kbn/triggers-actions-ui-plugin/public/types';
import { CasesConnectorFeatureId } from '@kbn/actions-plugin/common';
import type { CustomFieldConfiguration, TemplateConfiguration } from '../../../common/types/domain';
import type {
CustomFieldConfiguration,
TemplateConfiguration,
CustomFieldTypes,
} from '../../../common/types/domain';
import { useKibana } from '../../common/lib/kibana';
import { useGetActionTypes } from '../../containers/configure/use_action_types';
import { useGetCaseConfiguration } from '../../containers/configure/use_get_case_configuration';
@ -48,6 +52,8 @@ import { Templates } from '../templates';
import type { TemplateFormProps } from '../templates/types';
import { CustomFieldsForm } from '../custom_fields/form';
import { TemplateForm } from '../templates/form';
import type { CasesConfigurationUI, CaseUI } from '../../containers/types';
import { builderMap as customFieldsBuilderMap } from '../custom_fields/builder';
const sectionWrapperCss = css`
box-sizing: content-box;
@ -68,6 +74,40 @@ interface Flyout {
visible: boolean;
}
const addNewCustomFieldToTemplates = ({
templates,
customFields,
}: Pick<CasesConfigurationUI, 'templates' | 'customFields'>) => {
return templates.map((template) => {
const templateCustomFields = template.caseFields?.customFields ?? [];
customFields.forEach((field) => {
if (
!templateCustomFields.length ||
!templateCustomFields.find((templateCustomField) => templateCustomField.key === field.key)
) {
const customFieldFactory = customFieldsBuilderMap[field.type];
const { getDefaultValue } = customFieldFactory();
const value = getDefaultValue?.() ?? null;
templateCustomFields.push({
key: field.key,
type: field.type as CustomFieldTypes,
value: field.defaultValue ?? value,
} as CaseUI['customFields'][number]);
}
});
return {
...template,
caseFields: {
...template.caseFields,
customFields: [...templateCustomFields],
},
};
});
};
export const ConfigureCases: React.FC = React.memo(() => {
const { permissions } = useCasesContext();
const { triggersActionsUi } = useKibana().services;
@ -334,10 +374,16 @@ export const ConfigureCases: React.FC = React.memo(() => {
(data: CustomFieldConfiguration) => {
const updatedCustomFields = addOrReplaceField(customFields, data);
// add the new custom field to each template as well
const updatedTemplates = addNewCustomFieldToTemplates({
templates,
customFields: updatedCustomFields,
});
persistCaseConfigure({
connector,
customFields: updatedCustomFields,
templates,
templates: updatedTemplates,
id: configurationId,
version: configurationVersion,
closureType,

View file

@ -17,7 +17,7 @@ import {
import { css } from '@emotion/react';
import { useFormContext } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import type { CasePostRequest } from '../../../common';
import type { CasePostRequest, CaseUI } from '../../../common';
import type { ActionConnector } from '../../../common/types/domain';
import { Connector } from '../case_form_fields/connector';
import * as i18n from './translations';
@ -28,6 +28,7 @@ import { useCasesFeatures } from '../../common/use_cases_features';
import { TemplateSelector } from './templates';
import { getInitialCaseValue } from './utils';
import { CaseFormFields } from '../case_form_fields';
import { builderMap as customFieldsBuilderMap } from '../custom_fields/builder';
export interface CreateCaseFormFieldsProps {
configuration: CasesConfigurationUI;
@ -42,7 +43,22 @@ const transformTemplateCaseFieldsToCaseFormFields = (
caseTemplateFields: CasesConfigurationUITemplate['caseFields']
): CasePostRequest => {
const caseFields = removeEmptyFields(caseTemplateFields ?? {});
return getInitialCaseValue({ owner, ...caseFields });
const transFormedCustomFields = caseFields?.customFields?.map((customField) => {
const customFieldFactory = customFieldsBuilderMap[customField.type];
const { convertNullToEmpty } = customFieldFactory();
const value = convertNullToEmpty ? convertNullToEmpty(customField.value) : customField.value;
return {
...customField,
value,
};
});
return getInitialCaseValue({
owner,
...caseFields,
customFields: transFormedCustomFields as CaseUI['customFields'],
});
};
const DEFAULT_EMPTY_TEMPLATE_KEY = 'defaultEmptyTemplateKey';

View file

@ -20,6 +20,7 @@ describe('configureTextCustomFieldFactory ', () => {
label: 'Text',
getEuiTableColumn: expect.any(Function),
build: expect.any(Function),
convertNullToEmpty: expect.any(Function),
});
});
});

View file

@ -25,4 +25,5 @@ export const configureTextCustomFieldFactory: CustomFieldFactory<CaseCustomField
View,
Create,
}),
convertNullToEmpty: (value: string | boolean | null) => (value == null ? '' : String(value)),
});

View file

@ -24,6 +24,7 @@ describe('configureToggleCustomFieldFactory ', () => {
{ key: 'on', label: 'On', value: true },
{ key: 'off', label: 'Off', value: false },
],
getDefaultValue: expect.any(Function),
});
});
});

View file

@ -30,4 +30,5 @@ export const configureToggleCustomFieldFactory: CustomFieldFactory<CaseCustomFie
{ key: 'on', label: i18n.TOGGLE_FIELD_ON_LABEL, value: true },
{ key: 'off', label: i18n.TOGGLE_FIELD_OFF_LABEL, value: false },
],
getDefaultValue: () => false,
});

View file

@ -54,6 +54,8 @@ export type CustomFieldFactory<T extends CaseUICustomField> = () => {
getEuiTableColumn: (params: { label: string }) => CustomFieldEuiTableColumn;
build: () => CustomFieldType<T>;
filterOptions?: CustomFieldFactoryFilterOption[];
getDefaultValue?: () => string | boolean | null;
convertNullToEmpty?: (value: string | boolean | null) => string;
};
export type CustomFieldBuilderMap = {

View file

@ -561,6 +561,11 @@ describe('TemplateForm', () => {
type: 'toggle',
value: true,
},
{
key: 'test_key_3',
type: 'text',
value: null,
},
{
key: 'test_key_4',
type: 'toggle',
@ -645,6 +650,11 @@ describe('TemplateForm', () => {
type: 'toggle',
value: true,
},
{
key: 'test_key_3',
type: 'text',
value: null,
},
{
key: 'test_key_4',
type: 'toggle',

View file

@ -117,9 +117,9 @@ describe('utils', () => {
name: 'template 1',
templateDescription: '',
customFields: {
custom_field_1: 'foobar',
custom_fields_2: '',
custom_field_3: true,
test_key_1: 'foobar',
test_key_3: '',
test_key_2: true,
},
});
@ -131,7 +131,11 @@ describe('utils', () => {
name: 'none',
type: '.none',
},
customFields: [],
customFields: [
{ key: 'test_key_1', type: 'text', value: 'foobar' },
{ key: 'test_key_3', type: 'text', value: null },
{ key: 'test_key_2', type: 'toggle', value: true },
],
settings: {
syncAlerts: false,
},

View file

@ -79,14 +79,19 @@ export const templateSerializer = (
return data;
}
const { fields: connectorFields = null, key, name, ...rest } = data;
const {
fields: connectorFields = null,
key,
name,
customFields: templateCustomFields,
...rest
} = data;
const serializedConnectorFields = getConnectorsFormSerializer({ fields: connectorFields });
const nonEmptyFields = removeEmptyFields({ ...rest });
const {
connectorId,
customFields: templateCustomFields,
syncAlerts = false,
templateTags,
templateDescription,

View file

@ -556,51 +556,6 @@ describe('client', () => {
});
describe('customFields', () => {
it('throws when there are no customFields in configure and template has customField in the request', async () => {
clientArgs.services.caseConfigureService.get.mockResolvedValue({
// @ts-ignore: these are all the attributes needed for the test
attributes: {
templates: [
{
key: 'template_1',
name: 'template 1',
description: 'this is test description',
caseFields: null,
},
],
},
});
await expect(
update(
'test-id',
{
version: 'test-version',
templates: [
{
key: 'template_1',
name: 'template 1',
description: 'this is test description',
caseFields: {
customFields: [
{
key: 'custom_field_key_1',
type: CustomFieldTypes.TEXT,
value: 'custom field value 1',
},
],
},
},
],
},
clientArgs,
casesClientInternal
)
).rejects.toThrow(
'Failed to get patch configure in route: Error: No custom fields configured.'
);
});
it('throws when template has duplicated custom field keys in the request', async () => {
clientArgs.services.caseConfigureService.get.mockResolvedValue({
// @ts-ignore: these are all the attributes needed for the test
@ -637,6 +592,14 @@ describe('client', () => {
'test-id',
{
version: 'test-version',
customFields: [
{
key: 'custom_field_key_1',
label: 'text label',
type: CustomFieldTypes.TEXT,
required: false,
},
],
templates: [
{
key: 'template_1',
@ -667,67 +630,6 @@ describe('client', () => {
);
});
it('throws when there are invalid customField keys in the request', async () => {
clientArgs.services.caseConfigureService.get.mockResolvedValue({
// @ts-ignore: these are all the attributes needed for the test
attributes: {
customFields: [
{
key: 'custom_field_key_1',
label: 'text label',
type: CustomFieldTypes.TEXT,
required: false,
},
],
templates: [
{
key: 'template_1',
name: 'template 1',
description: 'this is test description',
caseFields: {
customFields: [
{
key: 'custom_field_key_1',
type: CustomFieldTypes.TEXT,
value: 'custom field value 1',
},
],
},
},
],
},
});
await expect(
update(
'test-id',
{
version: 'test-version',
templates: [
{
key: 'template_1',
name: 'template 1',
description: 'this is test description',
caseFields: {
customFields: [
{
key: 'custom_field_key_2',
type: CustomFieldTypes.TEXT,
value: 'custom field value 1',
},
],
},
},
],
},
clientArgs,
casesClientInternal
)
).rejects.toThrow(
'Failed to get patch configure in route: Error: Invalid custom field keys: custom_field_key_2'
);
});
it('throws when template has customField with invalid type in the request', async () => {
clientArgs.services.caseConfigureService.get.mockResolvedValue({
// @ts-ignore: these are all the attributes needed for the test
@ -764,6 +666,14 @@ describe('client', () => {
'test-id',
{
version: 'test-version',
customFields: [
{
key: 'custom_field_key_1',
label: 'text label',
type: CustomFieldTypes.TEXT,
required: false,
},
],
templates: [
{
key: 'template_1',
@ -789,6 +699,334 @@ describe('client', () => {
);
});
it('adds new custom field to template when configuration custom fields have new custom field', async () => {
clientArgs.services.caseConfigureService.get.mockResolvedValue({
// @ts-ignore: these are all the attributes needed for the test
attributes: {
connector: {
id: 'none',
name: 'none',
type: ConnectorTypes.none,
fields: null,
},
customFields: [],
templates: [],
closure_type: 'close-by-user',
owner: 'cases',
},
id: 'test-id',
version: 'test-version',
});
await update(
'test-id',
{
version: 'test-version',
customFields: [
{
key: 'custom_field_key_1',
label: 'text label',
type: CustomFieldTypes.TEXT,
required: false,
defaultValue: 'custom field 1 default value 1',
},
],
templates: [
{
key: 'template_1',
name: 'template 1',
description: 'this is test description',
caseFields: null,
},
],
},
clientArgs,
casesClientInternal
);
expect(clientArgs.services.caseConfigureService.patch).toHaveBeenCalledWith({
configurationId: 'test-id',
originalConfiguration: {
attributes: {
closure_type: 'close-by-user',
connector: {
fields: null,
id: 'none',
name: 'none',
type: '.none',
},
customFields: [],
owner: 'cases',
templates: [],
},
id: 'test-id',
version: 'test-version',
},
unsecuredSavedObjectsClient: expect.anything(),
updatedAttributes: {
customFields: [
{
key: 'custom_field_key_1',
label: 'text label',
type: CustomFieldTypes.TEXT,
required: false,
defaultValue: 'custom field 1 default value 1',
},
],
templates: [
{
caseFields: {
customFields: [
{
key: 'custom_field_key_1',
type: CustomFieldTypes.TEXT,
value: 'custom field 1 default value 1',
},
],
},
description: 'this is test description',
key: 'template_1',
name: 'template 1',
},
],
updated_at: expect.anything(),
updated_by: expect.anything(),
},
});
});
it('updates default value of existing custom fields in the configuration correctly', async () => {
clientArgs.services.caseConfigureService.get.mockResolvedValue({
// @ts-ignore: these are all the attributes needed for the test
attributes: {
connector: {
id: 'none',
name: 'none',
type: ConnectorTypes.none,
fields: null,
},
customFields: [
{
key: 'custom_field_key_1',
label: 'text label',
type: CustomFieldTypes.TEXT,
required: false,
defaultValue: 'custom field 1 default value 1',
},
],
templates: [
{
key: 'template_1',
name: 'template 1',
description: 'this is test description',
caseFields: {
customFields: [
{
key: 'custom_field_key_1',
type: CustomFieldTypes.TEXT,
value: 'custom field 1 default value 1',
},
],
},
},
],
closure_type: 'close-by-user',
owner: 'cases',
},
id: 'test-id',
version: 'test-version',
});
await update(
'test-id',
{
version: 'test-version',
customFields: [
{
key: 'custom_field_key_1',
label: 'text label',
type: CustomFieldTypes.TEXT,
required: false,
defaultValue: 'updated default value!!',
},
],
templates: [
{
key: 'template_1',
name: 'template 1',
description: 'this is test description',
caseFields: {
customFields: [
{
key: 'custom_field_key_1',
type: CustomFieldTypes.TEXT,
value: 'custom field 1 default value 1',
},
],
},
},
],
},
clientArgs,
casesClientInternal
);
expect(clientArgs.services.caseConfigureService.patch).toHaveBeenCalledWith({
configurationId: 'test-id',
originalConfiguration: {
attributes: {
closure_type: 'close-by-user',
connector: {
fields: null,
id: 'none',
name: 'none',
type: '.none',
},
owner: 'cases',
customFields: [
{
key: 'custom_field_key_1',
label: 'text label',
type: CustomFieldTypes.TEXT,
required: false,
defaultValue: 'custom field 1 default value 1',
},
],
templates: [
{
key: 'template_1',
name: 'template 1',
description: 'this is test description',
caseFields: {
customFields: [
{
key: 'custom_field_key_1',
type: CustomFieldTypes.TEXT,
value: 'custom field 1 default value 1',
},
],
},
},
],
},
id: 'test-id',
version: 'test-version',
},
unsecuredSavedObjectsClient: expect.anything(),
updatedAttributes: {
customFields: [
{
key: 'custom_field_key_1',
label: 'text label',
type: CustomFieldTypes.TEXT,
required: false,
defaultValue: 'updated default value!!',
},
],
templates: [
{
caseFields: {
customFields: [
{
key: 'custom_field_key_1',
type: CustomFieldTypes.TEXT,
value: 'custom field 1 default value 1',
},
],
},
description: 'this is test description',
key: 'template_1',
name: 'template 1',
},
],
updated_at: expect.anything(),
updated_by: expect.anything(),
},
});
});
it('removes custom field from template when there are no customFields in the request', async () => {
clientArgs.services.caseConfigureService.get.mockResolvedValue({
// @ts-ignore: these are all the attributes needed for the test
attributes: {
connector: {
id: 'none',
name: 'none',
type: ConnectorTypes.none,
fields: null,
},
customFields: [],
templates: [],
closure_type: 'close-by-user',
owner: 'cases',
},
id: 'test-id',
version: 'test-version',
});
await update(
'test-id',
{
version: 'test-version',
customFields: [],
templates: [
{
key: 'template_1',
name: 'template 1',
description: 'this is test description',
caseFields: {
customFields: [
{
key: 'custom_field_key_1',
type: CustomFieldTypes.TEXT,
value: 'custom field value 1',
},
],
},
},
],
},
clientArgs,
casesClientInternal
);
expect(clientArgs.services.caseConfigureService.patch).toHaveBeenCalledWith({
configurationId: 'test-id',
originalConfiguration: {
attributes: {
closure_type: 'close-by-user',
connector: {
fields: null,
id: 'none',
name: 'none',
type: '.none',
},
customFields: [],
owner: 'cases',
templates: [],
},
id: 'test-id',
version: 'test-version',
},
unsecuredSavedObjectsClient: expect.anything(),
updatedAttributes: {
customFields: [],
templates: [
{
key: 'template_1',
name: 'template 1',
description: 'this is test description',
caseFields: {
customFields: [],
},
},
],
updated_at: expect.anything(),
updated_by: expect.anything(),
},
});
});
it('removes deleted custom field from template correctly', async () => {
clientArgs.services.caseConfigureService.get.mockResolvedValue({
// @ts-ignore: these are all the attributes needed for the test
@ -1166,7 +1404,7 @@ describe('client', () => {
).resolves.not.toThrow();
});
it('throws when there are no customFields in configure and template has customField in the request', async () => {
it('throws error when there are no customFields but template has custom fields in the request', async () => {
await expect(
create(
{
@ -1241,7 +1479,7 @@ describe('client', () => {
);
});
it('throws when there are invalid customField keys in the request', async () => {
it('throw error when there are new customFields in the request but template does not have custom fields', async () => {
await expect(
create(
{
@ -1259,15 +1497,7 @@ describe('client', () => {
key: 'template_1',
name: 'template 1',
description: 'this is test description',
caseFields: {
customFields: [
{
key: 'custom_field_key_2',
type: CustomFieldTypes.TEXT,
value: 'custom field value 1',
},
],
},
caseFields: null,
},
],
},
@ -1275,7 +1505,7 @@ describe('client', () => {
casesClientInternal
)
).rejects.toThrow(
'Failed to create case configuration: Error: Invalid custom field keys: custom_field_key_2'
'Failed to create case configuration: Error: No custom fields added to template.'
);
});

View file

@ -44,7 +44,7 @@ import type { CasesClientArgs } from '../types';
import { getMappings } from './get_mappings';
import { Operations } from '../../authorization';
import { combineAuthorizedAndOwnerFilter, removeCustomFieldFromTemplates } from '../utils';
import { combineAuthorizedAndOwnerFilter, transformTemplateCustomFields } from '../utils';
import type { MappingsArgs, CreateMappingsArgs, UpdateMappingsArgs } from './types';
import { createMappings } from './create_mappings';
import { updateMappings } from './update_mappings';
@ -320,14 +320,14 @@ export async function update(
originalCustomFields: configuration.attributes.customFields,
});
await validateTemplates({
const updatedTemplates = transformTemplateCustomFields({
templates,
clientArgs,
customFields: configuration.attributes.customFields,
customFields: request.customFields,
});
const updatedTemplates = removeCustomFieldFromTemplates({
templates,
await validateTemplates({
templates: updatedTemplates,
clientArgs,
customFields: request.customFields,
});

View file

@ -198,6 +198,35 @@ describe('validators', () => {
).toThrowErrorMatchingInlineSnapshot(`"No custom fields configured."`);
});
it('throws if configuration has custom fields and template has no custom fields', () => {
expect(() =>
validateTemplatesCustomFieldsInRequest({
templates: [
{
key: 'template_key_1',
name: 'first template',
description: 'this is a first template value',
caseFields: null,
},
],
customFieldsConfiguration: [
{
key: 'first_key',
type: CustomFieldTypes.TEXT,
label: 'foo',
required: false,
},
{
key: 'second_key',
type: CustomFieldTypes.TOGGLE,
label: 'foo',
required: false,
},
],
})
).toThrowErrorMatchingInlineSnapshot(`"No custom fields added to template."`);
});
it('throws for a single invalid type', () => {
expect(() =>
validateTemplatesCustomFieldsInRequest({

View file

@ -60,20 +60,21 @@ export const validateTemplatesCustomFieldsInRequest = ({
}
templates.forEach((template, index) => {
if (
!template.caseFields ||
!template.caseFields.customFields ||
!template.caseFields.customFields.length
) {
return;
}
if (customFieldsConfiguration === undefined) {
if (customFieldsConfiguration === undefined && template.caseFields?.customFields?.length) {
throw Boom.badRequest('No custom fields configured.');
}
if (
(!template.caseFields ||
!template.caseFields.customFields ||
!template.caseFields.customFields.length) &&
customFieldsConfiguration?.length
) {
throw Boom.badRequest('No custom fields added to template.');
}
const params = {
requestCustomFields: template.caseFields.customFields,
requestCustomFields: template?.caseFields?.customFields,
customFieldsConfiguration,
};

View file

@ -19,7 +19,7 @@ import {
constructQueryOptions,
constructSearch,
convertSortField,
removeCustomFieldFromTemplates,
transformTemplateCustomFields,
} from './utils';
import { CasePersistedSeverity, CasePersistedStatus } from '../common/types/case';
import type { CustomFieldsConfiguration } from '../../common/types/domain';
@ -1132,7 +1132,7 @@ describe('utils', () => {
});
});
describe('removeCustomFieldFromTemplates', () => {
describe('transformTemplateCustomFields', () => {
const customFields = [
{
type: CustomFieldTypes.TEXT as const,
@ -1204,7 +1204,7 @@ describe('utils', () => {
];
it('removes custom field from template correctly', () => {
const res = removeCustomFieldFromTemplates({
const res = transformTemplateCustomFields({
templates,
customFields: [customFields[0], customFields[1]],
});
@ -1253,7 +1253,7 @@ describe('utils', () => {
});
it('removes multiple custom fields from template correctly', () => {
const res = removeCustomFieldFromTemplates({
const res = transformTemplateCustomFields({
templates,
customFields: [customFields[0]],
});
@ -1292,7 +1292,7 @@ describe('utils', () => {
});
it('removes all custom fields from templates when custom fields are empty', () => {
const res = removeCustomFieldFromTemplates({
const res = transformTemplateCustomFields({
templates,
customFields: [],
});
@ -1319,7 +1319,7 @@ describe('utils', () => {
});
it('removes all custom fields from templates when custom fields are undefined', () => {
const res = removeCustomFieldFromTemplates({
const res = transformTemplateCustomFields({
templates,
customFields: undefined,
});
@ -1330,8 +1330,8 @@ describe('utils', () => {
]);
});
it('does not remove custom field when templates do not have custom fields', () => {
const res = removeCustomFieldFromTemplates({
it('adds custom fields to templates when templates do not have custom fields', () => {
const res = transformTemplateCustomFields({
templates: [
{
key: 'test_template_1',
@ -1353,7 +1353,20 @@ describe('utils', () => {
expect(res).toEqual([
{
caseFields: null,
caseFields: {
customFields: [
{
key: customFields[0].key,
type: customFields[0].type,
value: customFields[0].defaultValue,
},
{
key: customFields[1].key,
type: customFields[1].type,
value: customFields[1].defaultValue,
},
],
},
description: 'This is a first test template',
key: 'test_template_1',
name: 'First test template',
@ -1364,13 +1377,25 @@ describe('utils', () => {
caseFields: {
description: 'this is test',
title: 'Test title',
customFields: [
{
key: customFields[0].key,
type: customFields[0].type,
value: customFields[0].defaultValue,
},
{
key: customFields[1].key,
type: customFields[1].type,
value: customFields[1].defaultValue,
},
],
},
},
]);
});
it('does not remove custom field when templates have empty custom fields', () => {
const res = removeCustomFieldFromTemplates({
it('adds custom fields to templates when template custom fields are empty', () => {
const res = transformTemplateCustomFields({
templates: [
{
key: 'test_template_2',
@ -1382,7 +1407,7 @@ describe('utils', () => {
},
},
],
customFields: [customFields[0], customFields[1]],
customFields: [customFields[0], customFields[1], customFields[2]],
});
expect(res).toEqual([
@ -1392,14 +1417,149 @@ describe('utils', () => {
caseFields: {
title: 'Test title',
description: 'this is test',
customFields: [],
customFields: [
{
key: customFields[0].key,
type: customFields[0].type,
value: customFields[0].defaultValue,
},
{
key: customFields[1].key,
type: customFields[1].type,
value: customFields[1].defaultValue,
},
{
key: customFields[2].key,
type: customFields[2].type,
value: null,
},
],
},
},
]);
});
it('adds custom fields to templates with correct values', () => {
const res = transformTemplateCustomFields({
templates: [
{
key: 'test_template_2',
name: 'Second test template',
caseFields: {
title: 'Test title',
description: 'this is test',
customFields: [],
},
},
],
customFields: [
...customFields,
{
type: CustomFieldTypes.TOGGLE as const,
key: 'test_key_4',
label: 'My test label 4',
required: true,
},
],
});
expect(res).toEqual([
{
key: 'test_template_2',
name: 'Second test template',
caseFields: {
title: 'Test title',
description: 'this is test',
customFields: [
{
key: customFields[0].key,
type: customFields[0].type,
value: customFields[0].defaultValue,
},
{
key: customFields[1].key,
type: customFields[1].type,
value: customFields[1].defaultValue,
},
{
key: customFields[2].key,
type: customFields[2].type,
value: null,
},
{
type: CustomFieldTypes.TOGGLE as const,
key: 'test_key_4',
value: false,
},
],
},
},
]);
});
it('does not change the existing template custom field', () => {
const res = transformTemplateCustomFields({
templates: [
{
key: 'test_template_2',
name: 'Second test template',
caseFields: {
title: 'Test title',
description: 'this is test',
customFields: [
{
key: customFields[0].key,
type: CustomFieldTypes.TEXT as const,
value: 'updated text value',
},
{
key: customFields[1].key,
type: CustomFieldTypes.TOGGLE as const,
value: false,
},
{
key: customFields[2].key,
type: customFields[2].type,
value: null,
},
],
},
},
],
customFields,
});
expect(res).toEqual([
{
key: 'test_template_2',
name: 'Second test template',
caseFields: {
title: 'Test title',
description: 'this is test',
customFields: [
{
key: customFields[0].key,
type: customFields[0].type,
value: 'updated text value',
},
{
key: customFields[1].key,
type: customFields[1].type,
value: false,
},
{
key: customFields[2].key,
type: customFields[2].type,
value: null,
},
],
},
},
]);
});
it('does not remove custom field from empty templates', () => {
const res = removeCustomFieldFromTemplates({
const res = transformTemplateCustomFields({
templates: [],
customFields: [customFields[0], customFields[1]],
});
@ -1408,7 +1568,7 @@ describe('utils', () => {
});
it('returns empty array when templates are undefined', () => {
const res = removeCustomFieldFromTemplates({
const res = transformTemplateCustomFields({
templates: undefined,
customFields: [customFields[0], customFields[1]],
});

View file

@ -17,11 +17,13 @@ import { nodeBuilder, fromKueryExpression, escapeKuery } from '@kbn/es-query';
import { spaceIdToNamespace } from '@kbn/spaces-plugin/server/lib/utils/namespace';
import type {
CaseCustomField,
CaseSeverity,
CaseStatuses,
CustomFieldsConfiguration,
ExternalReferenceAttachmentPayload,
TemplatesConfiguration,
CustomFieldTypes,
} from '../../common/types/domain';
import {
ActionsAttachmentPayloadRt,
@ -607,9 +609,9 @@ export const constructSearch = (
};
/**
* remove deleted custom field from template
* remove deleted custom field from template or add newly added custom field to template
*/
export const removeCustomFieldFromTemplates = ({
export const transformTemplateCustomFields = ({
templates,
customFields,
}: {
@ -621,21 +623,40 @@ export const removeCustomFieldFromTemplates = ({
}
return templates.map((template) => {
if (!template.caseFields?.customFields || !template.caseFields?.customFields.length) {
return template;
}
const templateCustomFields = template.caseFields?.customFields ?? [];
if (!customFields || !customFields?.length) {
if (!customFields || !customFields.length) {
return { ...template, caseFields: { ...template.caseFields, customFields: [] } };
}
const templateCustomFields = template.caseFields.customFields.filter((templateCustomField) =>
// remove deleted custom field from template
const transformedTemplateCustomFields = templateCustomFields.filter((templateCustomField) =>
customFields?.find((customField) => customField.key === templateCustomField.key)
);
// add new custom fields to template
if (customFields.length >= transformedTemplateCustomFields.length) {
customFields.forEach((field) => {
if (
!transformedTemplateCustomFields.find(
(templateCustomField) => templateCustomField.key === field.key
)
) {
const { getDefaultValue } = casesCustomFields.get(field.type) ?? {};
const value = getDefaultValue?.() ?? null;
transformedTemplateCustomFields.push({
key: field.key,
type: field.type as CustomFieldTypes,
value: field.defaultValue ?? value,
} as CaseCustomField);
}
});
}
return {
...template,
caseFields: { ...template.caseFields, customFields: templateCustomFields },
caseFields: { ...template.caseFields, customFields: transformedTemplateCustomFields },
};
});
};

View file

@ -19,4 +19,5 @@ export const getCasesToggleCustomField = () => ({
}
});
},
getDefaultValue: () => false,
});

View file

@ -12,6 +12,7 @@ export interface ICasesCustomField {
isSortable: boolean;
savedObjectMappingType: string;
validateFilteringValues: (values: Array<string | number | boolean | null>) => void;
getDefaultValue?: () => boolean | string | null;
}
export interface CasesCustomFieldsMap {

View file

@ -85,46 +85,48 @@ export default ({ getService }: FtrProviderContext): void => {
});
it('should return a configuration with templates', async () => {
const mockTemplates = [
{
key: 'test_template_1',
name: 'First test template',
description: 'This is a first test template',
tags: [],
caseFields: null,
},
{
key: 'test_template_2',
name: 'Second test template',
description: 'This is a second test template',
tags: ['foobar'],
caseFields: {
title: 'Case with sample template 2',
description: 'case desc',
severity: CaseSeverity.LOW,
category: null,
tags: ['sample-4'],
assignees: [],
customFields: [],
connector: {
id: 'none',
name: 'My Connector',
type: ConnectorTypes.none,
fields: null,
},
},
},
{
key: 'test_template_3',
name: 'Third test template',
description: 'This is a third test template',
caseFields: {
title: 'Case with sample template 3',
tags: ['sample-3'],
},
},
];
const templates = {
templates: [
{
key: 'test_template_1',
name: 'First test template',
description: 'This is a first test template',
tags: [],
caseFields: null,
},
{
key: 'test_template_2',
name: 'Second test template',
description: 'This is a second test template',
tags: ['foobar'],
caseFields: {
title: 'Case with sample template 2',
description: 'case desc',
severity: CaseSeverity.LOW,
category: null,
tags: ['sample-4'],
assignees: [],
customFields: [],
connector: {
id: 'none',
name: 'My Connector',
type: ConnectorTypes.none,
fields: null,
},
},
},
{
key: 'test_template_3',
name: 'Third test template',
description: 'This is a third test template',
caseFields: {
title: 'Case with sample template 3',
tags: ['sample-3'],
},
},
],
templates: mockTemplates,
};
await createConfiguration(
@ -136,7 +138,11 @@ export default ({ getService }: FtrProviderContext): void => {
const configuration = await getConfiguration({ supertest });
const data = removeServerGeneratedPropertiesFromSavedObject(configuration[0]);
expect(data).to.eql(getConfigurationOutput(false, templates));
expect(data).to.eql(
getConfigurationOutput(false, {
templates: mockTemplates,
})
);
});
it('should get a single configuration', async () => {

View file

@ -147,7 +147,7 @@ export default ({ getService }: FtrProviderContext): void => {
},
];
const templates = [
const mockTemplates = [
{
key: 'test_template_1',
name: 'First test template',
@ -196,23 +196,61 @@ export default ({ getService }: FtrProviderContext): void => {
tags: ['sample-3'],
},
},
] as ConfigurationPatchRequest['templates'];
];
const configuration = await createConfiguration(supertest, {
...getConfigurationRequest(),
customFields: customFieldsConfiguration as ConfigurationPatchRequest['customFields'],
});
const newConfiguration = await updateConfiguration(supertest, configuration.id, {
version: configuration.version,
customFields: customFieldsConfiguration,
templates,
templates: mockTemplates as ConfigurationPatchRequest['templates'],
});
const data = removeServerGeneratedPropertiesFromSavedObject(newConfiguration);
expect(data).to.eql({
...getConfigurationOutput(true),
customFields: customFieldsConfiguration as ConfigurationPatchRequest['customFields'],
templates,
templates: [
{
...mockTemplates[0],
caseFields: {
customFields: [
{
key: 'text_field_1',
type: CustomFieldTypes.TEXT,
value: null,
},
{
key: 'toggle_field_1',
value: false,
type: CustomFieldTypes.TOGGLE,
},
],
},
},
{ ...mockTemplates[1] },
{
...mockTemplates[2],
caseFields: {
...mockTemplates[2].caseFields,
customFields: [
{
key: 'text_field_1',
type: CustomFieldTypes.TEXT,
value: null,
},
{
key: 'toggle_field_1',
value: false,
type: CustomFieldTypes.TOGGLE,
},
],
},
},
] as ConfigurationPatchRequest['templates'],
});
});
@ -432,35 +470,6 @@ export default ({ getService }: FtrProviderContext): void => {
);
});
it("should not update a configuration with templates with custom fields that don't exist in the configuration", async () => {
const configuration = await createConfiguration(supertest);
await updateConfiguration(
supertest,
configuration.id,
{
version: configuration.version,
templates: [
{
key: 'test_template_1',
name: 'First test template',
description: 'This is a first test template',
caseFields: {
customFields: [
{
key: 'random_key',
type: CustomFieldTypes.TEXT,
value: 'Test',
},
],
},
},
],
},
400
);
});
it('should not patch a configuration with duplicated template keys', async () => {
const configuration = await createConfiguration(supertest);
await updateConfiguration(

View file

@ -123,7 +123,20 @@ export default ({ getService }: FtrProviderContext): void => {
key: 'test_template_1',
name: 'First test template',
description: 'This is a first test template',
caseFields: null,
caseFields: {
customFields: [
{
key: 'text_field_1',
type: CustomFieldTypes.TEXT,
value: null,
},
{
key: 'toggle_field_1',
value: false,
type: CustomFieldTypes.TOGGLE,
},
],
},
},
{
key: 'test_template_2',
@ -165,6 +178,18 @@ export default ({ getService }: FtrProviderContext): void => {
caseFields: {
title: 'Case with sample template 3',
tags: ['sample-3'],
customFields: [
{
key: 'text_field_1',
type: CustomFieldTypes.TEXT,
value: null,
},
{
key: 'toggle_field_1',
value: false,
type: CustomFieldTypes.TOGGLE,
},
],
},
},
];
@ -177,7 +202,11 @@ export default ({ getService }: FtrProviderContext): void => {
);
const data = removeServerGeneratedPropertiesFromSavedObject(configuration);
expect(data).to.eql({ ...getConfigurationOutput(false), customFields, templates });
expect(data).to.eql({
...getConfigurationOutput(false),
customFields,
templates,
});
});
it('should keep only the latest configuration', async () => {
@ -493,11 +522,40 @@ export default ({ getService }: FtrProviderContext): void => {
);
});
it("should not create a configuration with templates with custom fields that don't exist in the configuration", async () => {
it('should not create a configuration with duplicated template keys', async () => {
await createConfiguration(
supertest,
getConfigurationRequest({
overrides: {
templates: [
{
key: 'test_template_1',
name: 'First test template',
description: 'This is a first test template',
caseFields: null,
},
{
key: 'test_template_1',
name: 'Third test template',
description: 'This is a third test template',
caseFields: {
title: 'Case with sample template 3',
tags: ['sample-3'],
},
},
],
},
}),
400
);
});
it("should not create a configuration when templates have custom fields and custom fields don't exist in the configuration", async () => {
await createConfiguration(
supertest,
getConfigurationRequest({
overrides: {
customFields: [],
templates: [
{
key: 'test_template_1',
@ -520,11 +578,20 @@ export default ({ getService }: FtrProviderContext): void => {
);
});
it('should not create a configuration with duplicated template keys', async () => {
it('should not create a configuration when templates do not have custom fields and custom fields exist in the configuration', async () => {
await createConfiguration(
supertest,
getConfigurationRequest({
overrides: {
customFields: [
{
key: 'random_key',
type: CustomFieldTypes.TEXT,
label: 'New custom field',
defaultValue: 'Test',
required: true,
},
],
templates: [
{
key: 'test_template_1',
@ -532,15 +599,6 @@ export default ({ getService }: FtrProviderContext): void => {
description: 'This is a first test template',
caseFields: null,
},
{
key: 'test_template_1',
name: 'Third test template',
description: 'This is a third test template',
caseFields: {
title: 'Case with sample template 3',
tags: ['sample-3'],
},
},
],
},
}),