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

# Backport

This will backport the following commits from `main` to `8.15`:
- [[ResponseOps][Cases] Fix template's custom fields bugs
(#187591)](https://github.com/elastic/kibana/pull/187591)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Janki
Salvi","email":"117571355+js-jankisalvi@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-07-11T16:13:19Z","message":"[ResponseOps][Cases]
Fix template's custom fields bugs (#187591)\n\n## Summary\r\n\r\nFixes
https://github.com/elastic/kibana/issues/187333\r\n\r\n## Testing
behaviour: \r\nIssue 1: verify similar behaviour from API as
well.\r\n\r\n1. Create a template\r\n2. Add new toggle custom field with
default value as true\r\n3. Go to create case, See that new toggle
custom field has value: true\r\n4. Select recently created
template\r\n5. Toggle custom field new custom field with it's default
value\r\n\r\nIssue 2: verify similar behaviour from API as well.\r\n1.
Create a text custom field with default value\r\n2. Create a
template\r\n3. Set text custom field value to empty\r\n4. Save
template\r\n5. Go to create case\r\n6. Select recently created
template\r\n7. See that text custom field value is updated as per
template's custom\r\nfield
value","sha":"6b0d62805352c391fc7bfdb47ff848c0a46080ee","branchLabelMapping":{"^v8.16.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:skip","Team:ResponseOps","Feature:Cases","v8.15.0","v8.16.0"],"title":"[ResponseOps][Cases]
Fix template's custom fields
bugs","number":187591,"url":"https://github.com/elastic/kibana/pull/187591","mergeCommit":{"message":"[ResponseOps][Cases]
Fix template's custom fields bugs (#187591)\n\n## Summary\r\n\r\nFixes
https://github.com/elastic/kibana/issues/187333\r\n\r\n## Testing
behaviour: \r\nIssue 1: verify similar behaviour from API as
well.\r\n\r\n1. Create a template\r\n2. Add new toggle custom field with
default value as true\r\n3. Go to create case, See that new toggle
custom field has value: true\r\n4. Select recently created
template\r\n5. Toggle custom field new custom field with it's default
value\r\n\r\nIssue 2: verify similar behaviour from API as well.\r\n1.
Create a text custom field with default value\r\n2. Create a
template\r\n3. Set text custom field value to empty\r\n4. Save
template\r\n5. Go to create case\r\n6. Select recently created
template\r\n7. See that text custom field value is updated as per
template's custom\r\nfield
value","sha":"6b0d62805352c391fc7bfdb47ff848c0a46080ee"}},"sourceBranch":"main","suggestedTargetBranches":["8.15"],"targetPullRequestStates":[{"branch":"8.15","label":"v8.15.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/187591","number":187591,"mergeCommit":{"message":"[ResponseOps][Cases]
Fix template's custom fields bugs (#187591)\n\n## Summary\r\n\r\nFixes
https://github.com/elastic/kibana/issues/187333\r\n\r\n## Testing
behaviour: \r\nIssue 1: verify similar behaviour from API as
well.\r\n\r\n1. Create a template\r\n2. Add new toggle custom field with
default value as true\r\n3. Go to create case, See that new toggle
custom field has value: true\r\n4. Select recently created
template\r\n5. Toggle custom field new custom field with it's default
value\r\n\r\nIssue 2: verify similar behaviour from API as well.\r\n1.
Create a text custom field with default value\r\n2. Create a
template\r\n3. Set text custom field value to empty\r\n4. Save
template\r\n5. Go to create case\r\n6. Select recently created
template\r\n7. See that text custom field value is updated as per
template's custom\r\nfield
value","sha":"6b0d62805352c391fc7bfdb47ff848c0a46080ee"}}]}] BACKPORT-->

Co-authored-by: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2024-07-11 19:58:27 +02:00 committed by GitHub
parent 4806820b03
commit 124d22811c
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'],
});
};
export const CreateCaseFormFields: React.FC<CreateCaseFormFieldsProps> = React.memo(

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'],
},
},
],
},
}),