[ResponseOps][Cases] Introduce number custom field type (#195245)

Issue: https://github.com/elastic/kibana/issues/187208

In this PR I've added new number custom field. It includes both: FE and
BE.
Only safe integers (the safe integers consist of all integers from
-(2^53 - 1) to 2^53 - 1) are allowed as values.

Testing:
For testing Postman/Insomnia can be used.
Go to Case - Settings. New configure will be created. 
After that you can use this endpoint: 
`PATCH
7377ed43-af0c-46f1-bbe5-fd0b147d591d`

<details><summary>Body looks something like this:</summary>

{
    "closure_type": "close-by-user",
    "customFields": [
        {
            "type": "number",
            "key": "54d2abf2-be0e-4fec-ac33-cbce94cf1a10",
            "label": "num",
            "required": false,
            "defaultValue": 123
        },
        {
            "type": "number",
            "key": "6f165838-a8d2-49f7-bbf6-ab3ad96d0d46",
            "label": "num2",
            "required": false,
            "defaultValue": -10
        }
    ],
    "templates": [],
    "connector": {
        "id": "none",
        "type": ".none",
        "fields": null,
        "name": "none"
    },
    "version": "WzIyLDFd"
}

</details>

![Screenshot 2024-10-07 at 16 23
15](https://github.com/user-attachments/assets/2d769049-e339-47bb-a17d-189569b8785d)

Try different numbers: positive and negative. Try to add not number
types as a default value with `"type": "number"`

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Julia 2024-10-30 09:50:12 +01:00 committed by GitHub
parent 5576316aba
commit 7cad9c31f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
59 changed files with 2474 additions and 41 deletions

View file

@ -13,6 +13,7 @@ import {
limitedStringSchema,
NonEmptyString,
paginationSchema,
limitedNumberAsIntegerSchema,
} from '.';
import { MAX_DOCS_PER_PAGE } from '../constants';
@ -319,4 +320,69 @@ describe('schema', () => {
`);
});
});
describe('limitedNumberAsIntegerSchema', () => {
it('works correctly the number is safe integer', () => {
expect(PathReporter.report(limitedNumberAsIntegerSchema({ fieldName: 'foo' }).decode(1)))
.toMatchInlineSnapshot(`
Array [
"No errors!",
]
`);
});
it('fails when given a number that is lower than the minimum', () => {
expect(
PathReporter.report(
limitedNumberAsIntegerSchema({ fieldName: 'foo' }).decode(Number.MIN_SAFE_INTEGER - 1)
)
).toMatchInlineSnapshot(`
Array [
"The foo field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.",
]
`);
});
it('fails when given a number that is higher than the maximum', () => {
expect(
PathReporter.report(
limitedNumberAsIntegerSchema({ fieldName: 'foo' }).decode(Number.MAX_SAFE_INTEGER + 1)
)
).toMatchInlineSnapshot(`
Array [
"The foo field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.",
]
`);
});
it('fails when given a null instead of a number', () => {
expect(PathReporter.report(limitedNumberAsIntegerSchema({ fieldName: 'foo' }).decode(null)))
.toMatchInlineSnapshot(`
Array [
"Invalid value null supplied to : LimitedNumberAsInteger",
]
`);
});
it('fails when given a string instead of a number', () => {
expect(
PathReporter.report(
limitedNumberAsIntegerSchema({ fieldName: 'foo' }).decode('some string')
)
).toMatchInlineSnapshot(`
Array [
"Invalid value \\"some string\\" supplied to : LimitedNumberAsInteger",
]
`);
});
it('fails when given a float number instead of an safe integer number', () => {
expect(PathReporter.report(limitedNumberAsIntegerSchema({ fieldName: 'foo' }).decode(1.2)))
.toMatchInlineSnapshot(`
Array [
"The foo field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.",
]
`);
});
});
});

View file

@ -154,6 +154,24 @@ export const limitedNumberSchema = ({ fieldName, min, max }: LimitedSchemaType)
rt.identity
);
export const limitedNumberAsIntegerSchema = ({ fieldName }: { fieldName: string }) =>
new rt.Type<number, number, unknown>(
'LimitedNumberAsInteger',
rt.number.is,
(input, context) =>
either.chain(rt.number.validate(input, context), (s) => {
if (!Number.isSafeInteger(s)) {
return rt.failure(
input,
context,
`The ${fieldName} field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.`
);
}
return rt.success(s);
}),
rt.identity
);
export interface RegexStringSchemaType {
codec: rt.Type<string, string, unknown>;
pattern: string;

View file

@ -114,10 +114,15 @@ const basicCase: Case = {
value: true,
},
{
key: 'second_custom_field_key',
key: 'third_custom_field_key',
type: CustomFieldTypes.TEXT,
value: 'www.example.com',
},
{
key: 'fourth_custom_field_key',
type: CustomFieldTypes.NUMBER,
value: 3,
},
],
};
@ -149,6 +154,11 @@ describe('CasePostRequestRt', () => {
type: CustomFieldTypes.TOGGLE,
value: true,
},
{
key: 'third_custom_field_key',
type: CustomFieldTypes.NUMBER,
value: 3,
},
],
};
@ -322,6 +332,44 @@ describe('CasePostRequestRt', () => {
);
});
it(`throws an error when a number customFields is more than ${Number.MAX_SAFE_INTEGER}`, () => {
expect(
PathReporter.report(
CasePostRequestRt.decode({
...defaultRequest,
customFields: [
{
key: 'first_custom_field_key',
type: CustomFieldTypes.NUMBER,
value: Number.MAX_SAFE_INTEGER + 1,
},
],
})
)
).toContain(
`The value field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.`
);
});
it(`throws an error when a number customFields is less than ${Number.MIN_SAFE_INTEGER}`, () => {
expect(
PathReporter.report(
CasePostRequestRt.decode({
...defaultRequest,
customFields: [
{
key: 'first_custom_field_key',
type: CustomFieldTypes.NUMBER,
value: Number.MIN_SAFE_INTEGER - 1,
},
],
})
)
).toContain(
`The value field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.`
);
});
it('throws an error when a text customField is an empty string', () => {
expect(
PathReporter.report(
@ -665,6 +713,11 @@ describe('CasePatchRequestRt', () => {
type: 'toggle',
value: true,
},
{
key: 'third_custom_field_key',
type: 'number',
value: 123,
},
],
};

View file

@ -29,7 +29,11 @@ import {
NonEmptyString,
paginationSchema,
} from '../../../schema';
import { CaseCustomFieldToggleRt, CustomFieldTextTypeRt } from '../../domain';
import {
CaseCustomFieldToggleRt,
CustomFieldTextTypeRt,
CustomFieldNumberTypeRt,
} from '../../domain';
import {
CaseRt,
CaseSettingsRt,
@ -41,7 +45,10 @@ import {
import { CaseConnectorRt } from '../../domain/connector/v1';
import { CaseUserProfileRt, UserRt } from '../../domain/user/v1';
import { CasesStatusResponseRt } from '../stats/v1';
import { CaseCustomFieldTextWithValidationValueRt } from '../custom_field/v1';
import {
CaseCustomFieldTextWithValidationValueRt,
CaseCustomFieldNumberWithValidationValueRt,
} from '../custom_field/v1';
const CaseCustomFieldTextWithValidationRt = rt.strict({
key: rt.string,
@ -49,7 +56,17 @@ const CaseCustomFieldTextWithValidationRt = rt.strict({
value: rt.union([CaseCustomFieldTextWithValidationValueRt('value'), rt.null]),
});
const CustomFieldRt = rt.union([CaseCustomFieldTextWithValidationRt, CaseCustomFieldToggleRt]);
const CaseCustomFieldNumberWithValidationRt = rt.strict({
key: rt.string,
type: CustomFieldNumberTypeRt,
value: rt.union([CaseCustomFieldNumberWithValidationValueRt({ fieldName: 'value' }), rt.null]),
});
const CustomFieldRt = rt.union([
CaseCustomFieldTextWithValidationRt,
CaseCustomFieldToggleRt,
CaseCustomFieldNumberWithValidationRt,
]);
export const CaseRequestCustomFieldsRt = limitedArraySchema({
codec: CustomFieldRt,

View file

@ -36,6 +36,7 @@ import {
CustomFieldConfigurationWithoutTypeRt,
TextCustomFieldConfigurationRt,
ToggleCustomFieldConfigurationRt,
NumberCustomFieldConfigurationRt,
TemplateConfigurationRt,
} from './v1';
@ -79,6 +80,12 @@ describe('configure', () => {
type: CustomFieldTypes.TOGGLE,
required: false,
},
{
key: 'number_custom_field',
label: 'Number custom field',
type: CustomFieldTypes.NUMBER,
required: false,
},
],
};
const query = ConfigurationRequestRt.decode(request);
@ -512,6 +519,93 @@ describe('configure', () => {
});
});
describe('NumberCustomFieldConfigurationRt', () => {
const defaultRequest = {
key: 'my_number_custom_field',
label: 'Number Custom Field',
type: CustomFieldTypes.NUMBER,
required: true,
};
it('has expected attributes in request', () => {
const query = NumberCustomFieldConfigurationRt.decode(defaultRequest);
expect(query).toStrictEqual({
_tag: 'Right',
right: { ...defaultRequest },
});
});
it('has expected attributes in request with defaultValue', () => {
const query = NumberCustomFieldConfigurationRt.decode({
...defaultRequest,
defaultValue: 1,
});
expect(query).toStrictEqual({
_tag: 'Right',
right: { ...defaultRequest, defaultValue: 1 },
});
});
it('removes foo:bar attributes from request', () => {
const query = NumberCustomFieldConfigurationRt.decode({ ...defaultRequest, foo: 'bar' });
expect(query).toStrictEqual({
_tag: 'Right',
right: { ...defaultRequest },
});
});
it('defaultValue fails if the type is string', () => {
expect(
PathReporter.report(
NumberCustomFieldConfigurationRt.decode({
...defaultRequest,
defaultValue: 'string',
})
)[0]
).toContain('Invalid value "string" supplied');
});
it('defaultValue fails if the type is boolean', () => {
expect(
PathReporter.report(
NumberCustomFieldConfigurationRt.decode({
...defaultRequest,
defaultValue: false,
})
)[0]
).toContain('Invalid value false supplied');
});
it(`throws an error if the default value is more than ${Number.MAX_SAFE_INTEGER}`, () => {
expect(
PathReporter.report(
NumberCustomFieldConfigurationRt.decode({
...defaultRequest,
defaultValue: Number.MAX_SAFE_INTEGER + 1,
})
)[0]
).toContain(
'The defaultValue field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.'
);
});
it(`throws an error if the default value is less than ${Number.MIN_SAFE_INTEGER}`, () => {
expect(
PathReporter.report(
NumberCustomFieldConfigurationRt.decode({
...defaultRequest,
defaultValue: Number.MIN_SAFE_INTEGER - 1,
})
)[0]
).toContain(
'The defaultValue field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.'
);
});
});
describe('TemplateConfigurationRt', () => {
const defaultRequest = {
key: 'template_key_1',

View file

@ -18,12 +18,19 @@ import {
MAX_TEMPLATE_TAG_LENGTH,
} from '../../../constants';
import { limitedArraySchema, limitedStringSchema, regexStringRt } from '../../../schema';
import { CustomFieldTextTypeRt, CustomFieldToggleTypeRt } from '../../domain';
import {
CustomFieldTextTypeRt,
CustomFieldToggleTypeRt,
CustomFieldNumberTypeRt,
} from '../../domain';
import type { Configurations, Configuration } from '../../domain/configure/v1';
import { ConfigurationBasicWithoutOwnerRt, ClosureTypeRt } from '../../domain/configure/v1';
import { CaseConnectorRt } from '../../domain/connector/v1';
import { CaseBaseOptionalFieldsRequestRt } from '../case/v1';
import { CaseCustomFieldTextWithValidationValueRt } from '../custom_field/v1';
import {
CaseCustomFieldTextWithValidationValueRt,
CaseCustomFieldNumberWithValidationValueRt,
} from '../custom_field/v1';
export const CustomFieldConfigurationWithoutTypeRt = rt.strict({
/**
@ -64,8 +71,25 @@ export const ToggleCustomFieldConfigurationRt = rt.intersection([
),
]);
export const NumberCustomFieldConfigurationRt = rt.intersection([
rt.strict({ type: CustomFieldNumberTypeRt }),
CustomFieldConfigurationWithoutTypeRt,
rt.exact(
rt.partial({
defaultValue: rt.union([
CaseCustomFieldNumberWithValidationValueRt({ fieldName: 'defaultValue' }),
rt.null,
]),
})
),
]);
export const CustomFieldsConfigurationRt = limitedArraySchema({
codec: rt.union([TextCustomFieldConfigurationRt, ToggleCustomFieldConfigurationRt]),
codec: rt.union([
TextCustomFieldConfigurationRt,
ToggleCustomFieldConfigurationRt,
NumberCustomFieldConfigurationRt,
]),
min: 0,
max: MAX_CUSTOM_FIELDS_PER_CASE,
fieldName: 'customFields',

View file

@ -7,7 +7,11 @@
import { PathReporter } from 'io-ts/lib/PathReporter';
import { MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH } from '../../../constants';
import { CaseCustomFieldTextWithValidationValueRt, CustomFieldPutRequestRt } from './v1';
import {
CaseCustomFieldTextWithValidationValueRt,
CustomFieldPutRequestRt,
CaseCustomFieldNumberWithValidationValueRt,
} from './v1';
describe('Custom Fields', () => {
describe('CaseCustomFieldTextWithValidationValueRt', () => {
@ -100,4 +104,34 @@ describe('Custom Fields', () => {
).toContain('The value field cannot be an empty string.');
});
});
describe('CaseCustomFieldNumberWithValidationValueRt', () => {
const numberCustomFieldValueType = CaseCustomFieldNumberWithValidationValueRt({
fieldName: 'value',
});
it('should decode number correctly', () => {
const query = numberCustomFieldValueType.decode(123);
expect(query).toStrictEqual({
_tag: 'Right',
right: 123,
});
});
it('should not be more than Number.MAX_SAFE_INTEGER', () => {
expect(
PathReporter.report(numberCustomFieldValueType.decode(Number.MAX_SAFE_INTEGER + 1))[0]
).toContain(
'The value field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.'
);
});
it('should not be less than Number.MIN_SAFE_INTEGER', () => {
expect(
PathReporter.report(numberCustomFieldValueType.decode(Number.MIN_SAFE_INTEGER - 1))[0]
).toContain(
'The value field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.'
);
});
});
});

View file

@ -7,7 +7,7 @@
import * as rt from 'io-ts';
import { MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH } from '../../../constants';
import { limitedStringSchema } from '../../../schema';
import { limitedStringSchema, limitedNumberAsIntegerSchema } from '../../../schema';
export const CaseCustomFieldTextWithValidationValueRt = (fieldName: string) =>
limitedStringSchema({
@ -16,12 +16,22 @@ export const CaseCustomFieldTextWithValidationValueRt = (fieldName: string) =>
max: MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH,
});
export const CaseCustomFieldNumberWithValidationValueRt = ({ fieldName }: { fieldName: string }) =>
limitedNumberAsIntegerSchema({
fieldName,
});
/**
* Update custom_field
*/
export const CustomFieldPutRequestRt = rt.strict({
value: rt.union([rt.boolean, rt.null, CaseCustomFieldTextWithValidationValueRt('value')]),
value: rt.union([
rt.boolean,
rt.null,
CaseCustomFieldTextWithValidationValueRt('value'),
CaseCustomFieldNumberWithValidationValueRt({ fieldName: 'value' }),
]),
caseVersion: rt.string,
});

View file

@ -85,6 +85,11 @@ const basicCase = {
type: 'toggle',
value: true,
},
{
key: 'third_custom_field_key',
type: 'number',
value: 0,
},
],
};
@ -193,6 +198,11 @@ describe('CaseAttributesRt', () => {
type: 'toggle',
value: true,
},
{
key: 'third_custom_field_key',
type: 'number',
value: 0,
},
],
};

View file

@ -16,6 +16,7 @@ import {
TemplateConfigurationRt,
TextCustomFieldConfigurationRt,
ToggleCustomFieldConfigurationRt,
NumberCustomFieldConfigurationRt,
} from './v1';
describe('configure', () => {
@ -47,6 +48,13 @@ describe('configure', () => {
required: false,
};
const numberCustomField = {
key: 'number_custom_field',
label: 'Number custom field',
type: CustomFieldTypes.NUMBER,
required: false,
};
const templateWithAllCaseFields = {
key: 'template_sample_1',
name: 'Sample template 1',
@ -98,7 +106,7 @@ describe('configure', () => {
const defaultRequest = {
connector: resilient,
closure_type: 'close-by-user',
customFields: [textCustomField, toggleCustomField],
customFields: [textCustomField, toggleCustomField, numberCustomField],
templates: [],
owner: 'cases',
created_at: '2020-02-19T23:06:33.798Z',
@ -122,7 +130,7 @@ describe('configure', () => {
_tag: 'Right',
right: {
...defaultRequest,
customFields: [textCustomField, toggleCustomField],
customFields: [textCustomField, toggleCustomField, numberCustomField],
},
});
});
@ -134,7 +142,7 @@ describe('configure', () => {
_tag: 'Right',
right: {
...defaultRequest,
customFields: [textCustomField, toggleCustomField],
customFields: [textCustomField, toggleCustomField, numberCustomField],
},
});
});
@ -142,14 +150,14 @@ describe('configure', () => {
it('removes foo:bar attributes from custom fields', () => {
const query = ConfigurationAttributesRt.decode({
...defaultRequest,
customFields: [{ ...textCustomField, foo: 'bar' }, toggleCustomField],
customFields: [{ ...textCustomField, foo: 'bar' }, toggleCustomField, numberCustomField],
});
expect(query).toStrictEqual({
_tag: 'Right',
right: {
...defaultRequest,
customFields: [textCustomField, toggleCustomField],
customFields: [textCustomField, toggleCustomField, numberCustomField],
},
});
});
@ -351,6 +359,62 @@ describe('configure', () => {
});
});
describe('NumberCustomFieldConfigurationRt', () => {
const defaultRequest = {
key: 'my_number_custom_field',
label: 'Number Custom Field',
type: CustomFieldTypes.NUMBER,
required: false,
};
it('has expected attributes in request with required: false', () => {
const query = NumberCustomFieldConfigurationRt.decode(defaultRequest);
expect(query).toStrictEqual({
_tag: 'Right',
right: { ...defaultRequest },
});
});
it('has expected attributes in request with defaultValue and required: true', () => {
const query = NumberCustomFieldConfigurationRt.decode({
...defaultRequest,
required: true,
defaultValue: 0,
});
expect(query).toStrictEqual({
_tag: 'Right',
right: {
...defaultRequest,
required: true,
defaultValue: 0,
},
});
});
it('defaultValue fails if the type is not number', () => {
expect(
PathReporter.report(
NumberCustomFieldConfigurationRt.decode({
...defaultRequest,
required: true,
defaultValue: 'foobar',
})
)[0]
).toContain('Invalid value "foobar" supplied');
});
it('removes foo:bar attributes from request', () => {
const query = NumberCustomFieldConfigurationRt.decode({ ...defaultRequest, foo: 'bar' });
expect(query).toStrictEqual({
_tag: 'Right',
right: { ...defaultRequest },
});
});
});
describe('TemplateConfigurationRt', () => {
const defaultRequest = templateWithAllCaseFields;

View file

@ -8,7 +8,11 @@
import * as rt from 'io-ts';
import { CaseConnectorRt, ConnectorMappingsRt } from '../connector/v1';
import { UserRt } from '../user/v1';
import { CustomFieldTextTypeRt, CustomFieldToggleTypeRt } from '../custom_field/v1';
import {
CustomFieldTextTypeRt,
CustomFieldToggleTypeRt,
CustomFieldNumberTypeRt,
} from '../custom_field/v1';
import { CaseBaseOptionalFieldsRt } from '../case/v1';
export const ClosureTypeRt = rt.union([
@ -51,9 +55,20 @@ export const ToggleCustomFieldConfigurationRt = rt.intersection([
),
]);
export const NumberCustomFieldConfigurationRt = rt.intersection([
rt.strict({ type: CustomFieldNumberTypeRt }),
CustomFieldConfigurationWithoutTypeRt,
rt.exact(
rt.partial({
defaultValue: rt.union([rt.number, rt.null]),
})
),
]);
export const CustomFieldConfigurationRt = rt.union([
TextCustomFieldConfigurationRt,
ToggleCustomFieldConfigurationRt,
NumberCustomFieldConfigurationRt,
]);
export const CustomFieldsConfigurationRt = rt.array(CustomFieldConfigurationRt);

View file

@ -42,6 +42,22 @@ describe('CaseCustomFieldRt', () => {
value: null,
},
],
[
'type number value number',
{
key: 'number_custom_field_1',
type: 'number',
value: 1,
},
],
[
'type number value null',
{
key: 'number_custom_field_2',
type: 'number',
value: null,
},
],
])(`has expected attributes for customField with %s`, (_, customField) => {
const query = CaseCustomFieldRt.decode(customField);
@ -70,4 +86,14 @@ describe('CaseCustomFieldRt', () => {
expect(PathReporter.report(query)[0]).toContain('Invalid value "hello" supplied');
});
it('fails if number type but value is a string', () => {
const query = CaseCustomFieldRt.decode({
key: 'list_custom_field_1',
type: 'number',
value: 'hi',
});
expect(PathReporter.report(query)[0]).toContain('Invalid value "hi" supplied');
});
});

View file

@ -9,10 +9,12 @@ import * as rt from 'io-ts';
export enum CustomFieldTypes {
TEXT = 'text',
TOGGLE = 'toggle',
NUMBER = 'number',
}
export const CustomFieldTextTypeRt = rt.literal(CustomFieldTypes.TEXT);
export const CustomFieldToggleTypeRt = rt.literal(CustomFieldTypes.TOGGLE);
export const CustomFieldNumberTypeRt = rt.literal(CustomFieldTypes.NUMBER);
const CaseCustomFieldTextRt = rt.strict({
key: rt.string,
@ -26,10 +28,21 @@ export const CaseCustomFieldToggleRt = rt.strict({
value: rt.union([rt.boolean, rt.null]),
});
export const CaseCustomFieldRt = rt.union([CaseCustomFieldTextRt, CaseCustomFieldToggleRt]);
export const CaseCustomFieldNumberRt = rt.strict({
key: rt.string,
type: CustomFieldNumberTypeRt,
value: rt.union([rt.number, rt.null]),
});
export const CaseCustomFieldRt = rt.union([
CaseCustomFieldTextRt,
CaseCustomFieldToggleRt,
CaseCustomFieldNumberRt,
]);
export const CaseCustomFieldsRt = rt.array(CaseCustomFieldRt);
export type CaseCustomFields = rt.TypeOf<typeof CaseCustomFieldsRt>;
export type CaseCustomField = rt.TypeOf<typeof CaseCustomFieldRt>;
export type CaseCustomFieldToggle = rt.TypeOf<typeof CaseCustomFieldToggleRt>;
export type CaseCustomFieldText = rt.TypeOf<typeof CaseCustomFieldTextRt>;
export type CaseCustomFieldNumber = rt.TypeOf<typeof CaseCustomFieldNumberRt>;

View file

@ -300,6 +300,12 @@ export const MAX_LENGTH_ERROR = (field: string, length: number) =>
'The length of the {field} is too long. The maximum length is {length} characters.',
});
export const SAFE_INTEGER_NUMBER_ERROR = (field: string) =>
i18n.translate('xpack.cases.customFields.safeIntegerNumberError', {
values: { field },
defaultMessage: `The value of the {field} should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.`,
});
export const MAX_TAGS_ERROR = (length: number) =>
i18n.translate('xpack.cases.createCase.maxTagsError', {
values: { length },

View file

@ -78,7 +78,7 @@ describe.skip('CustomFields', () => {
</FormTestComponent>
);
expect(await screen.findAllByTestId('form-optional-field-label')).toHaveLength(2);
expect(await screen.findAllByTestId('form-optional-field-label')).toHaveLength(4);
});
it('should not set default value when in edit mode', async () => {
@ -115,12 +115,14 @@ describe.skip('CustomFields', () => {
const customFields = customFieldsWrapper.querySelectorAll('.euiFormRow');
expect(customFields).toHaveLength(4);
expect(customFields).toHaveLength(6);
expect(customFields[0]).toHaveTextContent('My test label 1');
expect(customFields[1]).toHaveTextContent('My test label 2');
expect(customFields[2]).toHaveTextContent('My test label 3');
expect(customFields[3]).toHaveTextContent('My test label 4');
expect(customFields[4]).toHaveTextContent('My test label 5');
expect(customFields[5]).toHaveTextContent('My test label 6');
});
it('should update the custom fields', async () => {
@ -132,6 +134,7 @@ describe.skip('CustomFields', () => {
const textField = customFieldsConfigurationMock[2];
const toggleField = customFieldsConfigurationMock[3];
const numberField = customFieldsConfigurationMock[5];
await userEvent.type(
await screen.findByTestId(`${textField.key}-${textField.type}-create-custom-field`),
@ -140,6 +143,10 @@ describe.skip('CustomFields', () => {
await userEvent.click(
await screen.findByTestId(`${toggleField.key}-${toggleField.type}-create-custom-field`)
);
await userEvent.type(
await screen.findByTestId(`${numberField.key}-${numberField.type}-create-custom-field`),
'4'
);
await userEvent.click(await screen.findByText('Submit'));
@ -152,6 +159,8 @@ describe.skip('CustomFields', () => {
[customFieldsConfigurationMock[1].key]: customFieldsConfigurationMock[1].defaultValue,
[textField.key]: 'hello',
[toggleField.key]: true,
[customFieldsConfigurationMock[4].key]: customFieldsConfigurationMock[4].defaultValue,
[numberField.key]: '4',
},
},
true

View file

@ -206,6 +206,7 @@ describe('CaseFormFields', () => {
const textField = customFieldsConfigurationMock[0];
const toggleField = customFieldsConfigurationMock[1];
const numberField = customFieldsConfigurationMock[4];
const textCustomField = await screen.findByTestId(
`${textField.key}-${textField.type}-create-custom-field`
@ -219,6 +220,13 @@ describe('CaseFormFields', () => {
await screen.findByTestId(`${toggleField.key}-${toggleField.type}-create-custom-field`)
);
const numberCustomField = await screen.findByTestId(
`${numberField.key}-${numberField.type}-create-custom-field`
);
await user.clear(numberCustomField);
await user.paste('4321');
await user.click(await screen.findByText('Submit'));
await waitFor(() => {
@ -230,6 +238,7 @@ describe('CaseFormFields', () => {
test_key_1: 'My text test value 1',
test_key_2: false,
test_key_4: false,
test_key_5: '4321',
},
},
true
@ -268,6 +277,7 @@ describe('CaseFormFields', () => {
test_key_1: 'Test custom filed value',
test_key_2: true,
test_key_4: false,
test_key_5: 123,
},
},
true

View file

@ -89,7 +89,7 @@ describe('Case View Page files tab', () => {
exact: false,
});
expect(customFields.length).toBe(4);
expect(customFields.length).toBe(6);
expect(await within(customFields[0]).findByRole('heading')).toHaveTextContent(
'My test label 1'
@ -103,6 +103,12 @@ describe('Case View Page files tab', () => {
expect(await within(customFields[3]).findByRole('heading')).toHaveTextContent(
'My test label 4'
);
expect(await within(customFields[4]).findByRole('heading')).toHaveTextContent(
'My test label 5'
);
expect(await within(customFields[5]).findByRole('heading')).toHaveTextContent(
'My test label 6'
);
});
it('pass the permissions to custom fields correctly', async () => {

View file

@ -612,6 +612,16 @@ describe('CommonFlyout ', () => {
type: 'toggle',
value: false,
},
{
key: 'test_key_5',
type: 'number',
value: 123,
},
{
key: 'test_key_6',
type: 'number',
value: null,
},
],
},
});

View file

@ -715,6 +715,8 @@ describe('ConfigureCases', () => {
{ ...customFieldsConfigurationMock[1] },
{ ...customFieldsConfigurationMock[2] },
{ ...customFieldsConfigurationMock[3] },
{ ...customFieldsConfigurationMock[4] },
{ ...customFieldsConfigurationMock[5] },
],
templates: [],
id: '',
@ -774,6 +776,8 @@ describe('ConfigureCases', () => {
{ ...customFieldsConfigurationMock[1] },
{ ...customFieldsConfigurationMock[2] },
{ ...customFieldsConfigurationMock[3] },
{ ...customFieldsConfigurationMock[4] },
{ ...customFieldsConfigurationMock[5] },
],
templates: [
{
@ -867,6 +871,16 @@ describe('ConfigureCases', () => {
type: customFieldsConfigurationMock[3].type,
value: false,
},
{
key: customFieldsConfigurationMock[4].key,
type: customFieldsConfigurationMock[4].type,
value: customFieldsConfigurationMock[4].defaultValue,
},
{
key: customFieldsConfigurationMock[5].key,
type: customFieldsConfigurationMock[5].type,
value: null,
},
{
key: expect.anything(),
type: CustomFieldTypes.TEXT as const,
@ -930,6 +944,8 @@ describe('ConfigureCases', () => {
{ ...customFieldsConfigurationMock[1] },
{ ...customFieldsConfigurationMock[2] },
{ ...customFieldsConfigurationMock[3] },
{ ...customFieldsConfigurationMock[4] },
{ ...customFieldsConfigurationMock[5] },
],
templates: [],
id: '',
@ -1107,6 +1123,16 @@ describe('ConfigureCases', () => {
type: customFieldsConfigurationMock[3].type,
value: false, // when no default value for toggle, we set it to false
},
{
key: customFieldsConfigurationMock[4].key,
type: customFieldsConfigurationMock[4].type,
value: customFieldsConfigurationMock[4].defaultValue,
},
{
key: customFieldsConfigurationMock[5].key,
type: customFieldsConfigurationMock[5].type,
value: null,
},
],
},
},

View file

@ -517,6 +517,7 @@ describe('Create case', () => {
const textField = customFieldsConfigurationMock[0];
const toggleField = customFieldsConfigurationMock[1];
const numberField = customFieldsConfigurationMock[4];
expect(await screen.findByTestId('caseCustomFields')).toBeInTheDocument();
@ -532,6 +533,14 @@ describe('Create case', () => {
await screen.findByTestId(`${toggleField.key}-${toggleField.type}-create-custom-field`)
);
const numberCustomField = await screen.findByTestId(
`${numberField.key}-${numberField.type}-create-custom-field`
);
await user.clear(numberCustomField);
await user.click(numberCustomField);
await user.paste('678');
await user.click(await screen.findByTestId('create-case-submit'));
await waitFor(() => expect(postCase).toHaveBeenCalled());
@ -544,6 +553,8 @@ describe('Create case', () => {
{ ...customFieldsMock[1], value: false }, // toggled the default
customFieldsMock[2],
{ ...customFieldsMock[3], value: false },
{ ...customFieldsMock[4], value: 678 },
customFieldsMock[5],
{
key: 'my_custom_field_key',
type: CustomFieldTypes.TEXT,

View file

@ -9,8 +9,10 @@ import type { CustomFieldBuilderMap } from './types';
import { CustomFieldTypes } from '../../../common/types/domain';
import { configureTextCustomFieldFactory } from './text/configure_text_field';
import { configureToggleCustomFieldFactory } from './toggle/configure_toggle_field';
import { configureNumberCustomFieldFactory } from './number/configure_number_field';
export const builderMap = Object.freeze({
[CustomFieldTypes.TEXT]: configureTextCustomFieldFactory,
[CustomFieldTypes.TOGGLE]: configureToggleCustomFieldFactory,
[CustomFieldTypes.NUMBER]: configureNumberCustomFieldFactory,
} as const) as CustomFieldBuilderMap;

View file

@ -59,13 +59,20 @@ describe('CustomFieldsList', () => {
)
).toBeInTheDocument();
expect((await screen.findAllByText('Text')).length).toBe(2);
expect((await screen.findAllByText('Required')).length).toBe(2);
expect((await screen.findAllByText('Required')).length).toBe(3);
expect(
await screen.findByTestId(
`custom-field-${customFieldsConfigurationMock[1].key}-${customFieldsConfigurationMock[1].type}`
)
).toBeInTheDocument();
expect((await screen.findAllByText('Toggle')).length).toBe(2);
expect(
await screen.findByTestId(
`custom-field-${customFieldsConfigurationMock[4].key}-${customFieldsConfigurationMock[4].type}`
)
).toBeInTheDocument();
expect((await screen.findAllByText('Number')).length).toBe(2);
});
it('shows single CustomFieldsList correctly', async () => {

View file

@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { FieldConfig } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
import { REQUIRED_FIELD, SAFE_INTEGER_NUMBER_ERROR } from '../translations';
const { emptyField } = fieldValidators;
export const getNumberFieldConfig = ({
required,
label,
defaultValue,
}: {
required: boolean;
label: string;
defaultValue?: number;
}): FieldConfig<number> => {
const validators = [];
if (required) {
validators.push({
validator: emptyField(REQUIRED_FIELD(label)),
});
}
return {
...(defaultValue && { defaultValue }),
validations: [
...validators,
{
validator: ({ value }) => {
if (value == null) {
return;
}
const numericValue = Number(value);
if (!Number.isSafeInteger(numericValue)) {
return { message: SAFE_INTEGER_NUMBER_ERROR(label) };
}
},
},
],
};
};

View file

@ -0,0 +1,108 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { FormTestComponent } from '../../../common/test_utils';
import * as i18n from '../translations';
import { Configure } from './configure';
describe('Configure ', () => {
const onSubmit = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
it('renders correctly', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Configure />
</FormTestComponent>
);
expect(screen.getByText(i18n.FIELD_OPTION_REQUIRED)).toBeInTheDocument();
});
it('updates field options without default value correctly when not required', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Configure />
</FormTestComponent>
);
await userEvent.click(await screen.findByTestId('form-test-component-submit-button'));
await waitFor(() => {
// data, isValid
expect(onSubmit).toBeCalledWith({}, true);
});
});
it('updates field options with default value correctly when not required', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Configure />
</FormTestComponent>
);
await userEvent.click(await screen.findByTestId('number-custom-field-default-value'));
await userEvent.paste('123');
await userEvent.click(await screen.findByTestId('form-test-component-submit-button'));
await waitFor(() => {
// data, isValid
expect(onSubmit).toBeCalledWith({ defaultValue: '123' }, true);
});
});
it('updates field options with default value correctly when required', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Configure />
</FormTestComponent>
);
await userEvent.click(await screen.findByTestId('number-custom-field-required'));
await userEvent.click(await screen.findByTestId('number-custom-field-default-value'));
await userEvent.paste('123');
await userEvent.click(await screen.findByTestId('form-test-component-submit-button'));
await waitFor(() => {
// data, isValid
expect(onSubmit).toBeCalledWith(
{
required: true,
defaultValue: '123',
},
true
);
});
});
it('updates field options without default value correctly when required', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Configure />
</FormTestComponent>
);
await userEvent.click(await screen.findByTestId('number-custom-field-required'));
await userEvent.click(await screen.findByTestId('form-test-component-submit-button'));
await waitFor(() => {
// data, isValid
expect(onSubmit).toBeCalledWith(
{
required: true,
},
true
);
});
});
});

View file

@ -0,0 +1,54 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import { CheckBoxField, NumericField } from '@kbn/es-ui-shared-plugin/static/forms/components';
import type { CaseCustomFieldNumber } from '../../../../common/types/domain';
import type { CustomFieldType } from '../types';
import { getNumberFieldConfig } from './config';
import * as i18n from '../translations';
const ConfigureComponent: CustomFieldType<CaseCustomFieldNumber>['Configure'] = () => {
const config = getNumberFieldConfig({
required: false,
label: i18n.DEFAULT_VALUE.toLocaleLowerCase(),
});
return (
<>
<UseField
path="required"
component={CheckBoxField}
componentProps={{
label: i18n.FIELD_OPTIONS,
'data-test-subj': 'number-custom-field-required-wrapper',
euiFieldProps: {
label: i18n.FIELD_OPTION_REQUIRED,
'data-test-subj': 'number-custom-field-required',
},
}}
/>
<UseField
path="defaultValue"
component={NumericField}
config={config}
componentProps={{
label: i18n.DEFAULT_VALUE,
euiFieldProps: {
'data-test-subj': 'number-custom-field-default-value',
step: 1,
},
}}
/>
</>
);
};
ConfigureComponent.displayName = 'Configure';
export const Configure = React.memo(ConfigureComponent);

View file

@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { configureNumberCustomFieldFactory } from './configure_number_field';
describe('configureTextCustomFieldFactory ', () => {
const builder = configureNumberCustomFieldFactory();
beforeEach(() => {
jest.clearAllMocks();
});
it('renders correctly', async () => {
expect(builder).toEqual({
id: 'number',
label: 'Number',
getEuiTableColumn: expect.any(Function),
build: expect.any(Function),
});
});
});

View file

@ -0,0 +1,28 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { CustomFieldFactory } from '../types';
import type { CaseCustomFieldNumber } from '../../../../common/types/domain';
import { CustomFieldTypes } from '../../../../common/types/domain';
import * as i18n from '../translations';
import { getEuiTableColumn } from './get_eui_table_column';
import { Edit } from './edit';
import { View } from './view';
import { Configure } from './configure';
import { Create } from './create';
export const configureNumberCustomFieldFactory: CustomFieldFactory<CaseCustomFieldNumber> = () => ({
id: CustomFieldTypes.NUMBER,
label: i18n.NUMBER_LABEL,
getEuiTableColumn,
build: () => ({
Configure,
Edit,
View,
Create,
}),
});

View file

@ -0,0 +1,225 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { FormTestComponent } from '../../../common/test_utils';
import { Create } from './create';
import { customFieldsConfigurationMock } from '../../../containers/mock';
describe('Create ', () => {
const onSubmit = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
// required number custom field with a default value
const customFieldConfiguration = customFieldsConfigurationMock[4];
it('renders correctly with default value and required', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Create isLoading={false} customFieldConfiguration={customFieldConfiguration} />
</FormTestComponent>
);
expect(await screen.findByText(customFieldConfiguration.label)).toBeInTheDocument();
expect(
await screen.findByTestId(`${customFieldConfiguration.key}-number-create-custom-field`)
).toHaveValue(customFieldConfiguration.defaultValue as number);
});
it('renders correctly without default value and not required', async () => {
const optionalField = customFieldsConfigurationMock[5]; // optional number custom field
render(
<FormTestComponent onSubmit={onSubmit}>
<Create isLoading={false} customFieldConfiguration={optionalField} />
</FormTestComponent>
);
expect(await screen.findByText(optionalField.label)).toBeInTheDocument();
expect(
await screen.findByTestId(`${optionalField.key}-number-create-custom-field`)
).toHaveValue(null);
});
it('does not render default value when setDefaultValue is false', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Create
isLoading={false}
customFieldConfiguration={customFieldConfiguration}
setDefaultValue={false}
/>
</FormTestComponent>
);
expect(
await screen.findByTestId(`${customFieldConfiguration.key}-number-create-custom-field`)
).toHaveValue(null);
});
it('renders loading state correctly', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Create isLoading={true} customFieldConfiguration={customFieldConfiguration} />
</FormTestComponent>
);
expect(await screen.findByRole('progressbar')).toBeInTheDocument();
});
it('disables the text when loading', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Create isLoading={true} customFieldConfiguration={customFieldConfiguration} />
</FormTestComponent>
);
expect(
await screen.findByTestId(`${customFieldConfiguration.key}-number-create-custom-field`)
).toHaveAttribute('disabled');
});
it('updates the value correctly', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Create isLoading={false} customFieldConfiguration={customFieldConfiguration} />
</FormTestComponent>
);
const numberCustomField = await screen.findByTestId(
`${customFieldConfiguration.key}-number-create-custom-field`
);
await userEvent.clear(numberCustomField);
await userEvent.click(numberCustomField);
await userEvent.paste('1234');
await userEvent.click(await screen.findByText('Submit'));
await waitFor(() => {
// data, isValid
expect(onSubmit).toHaveBeenCalledWith(
{
customFields: {
[customFieldConfiguration.key]: '1234',
},
},
true
);
});
});
it('shows error when number is too big', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Create isLoading={false} customFieldConfiguration={customFieldConfiguration} />
</FormTestComponent>
);
const numberCustomField = await screen.findByTestId(
`${customFieldConfiguration.key}-number-create-custom-field`
);
await userEvent.clear(numberCustomField);
await userEvent.click(numberCustomField);
await userEvent.paste(`${Number.MAX_SAFE_INTEGER + 1}`);
await userEvent.click(await screen.findByText('Submit'));
expect(
await screen.findByText(
'The value of the My test label 5 should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.'
)
).toBeInTheDocument();
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({}, false);
});
});
it('shows error when number is too small', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Create
isLoading={false}
customFieldConfiguration={{ ...customFieldConfiguration, required: false }}
/>
</FormTestComponent>
);
const numberCustomField = await screen.findByTestId(
`${customFieldConfiguration.key}-number-create-custom-field`
);
await userEvent.clear(numberCustomField);
await userEvent.click(numberCustomField);
await userEvent.paste(`${Number.MIN_SAFE_INTEGER - 1}`);
await userEvent.click(await screen.findByText('Submit'));
expect(
await screen.findByText(
'The value of the My test label 5 should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.'
)
).toBeInTheDocument();
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({}, false);
});
});
it('shows error when number is required but is empty', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Create
isLoading={false}
customFieldConfiguration={{ ...customFieldConfiguration, required: true }}
/>
</FormTestComponent>
);
await userEvent.clear(
await screen.findByTestId(`${customFieldConfiguration.key}-number-create-custom-field`)
);
await userEvent.click(await screen.findByText('Submit'));
expect(
await screen.findByText(`${customFieldConfiguration.label} is required.`)
).toBeInTheDocument();
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({}, false);
});
});
it('does not show error when number is not required but is empty', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Create
isLoading={false}
customFieldConfiguration={{
key: customFieldConfiguration.key,
type: customFieldConfiguration.type,
label: customFieldConfiguration.label,
required: false,
}}
/>
</FormTestComponent>
);
await userEvent.click(await screen.findByText('Submit'));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({}, true);
});
});
});

View file

@ -0,0 +1,52 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import { NumericField } from '@kbn/es-ui-shared-plugin/static/forms/components';
import type { CaseCustomFieldNumber } from '../../../../common/types/domain';
import type { CustomFieldType } from '../types';
import { getNumberFieldConfig } from './config';
import { OptionalFieldLabel } from '../../optional_field_label';
const CreateComponent: CustomFieldType<CaseCustomFieldNumber>['Create'] = ({
customFieldConfiguration,
isLoading,
setAsOptional,
setDefaultValue = true,
}) => {
const { key, label, required, defaultValue } = customFieldConfiguration;
const config = getNumberFieldConfig({
required: setAsOptional ? false : required,
label,
...(defaultValue &&
setDefaultValue &&
!isNaN(Number(defaultValue)) && { defaultValue: Number(defaultValue) }),
});
return (
<UseField
path={`customFields.${key}`}
config={config}
component={NumericField}
label={label}
componentProps={{
labelAppend: setAsOptional ? OptionalFieldLabel : null,
euiFieldProps: {
'data-test-subj': `${key}-number-create-custom-field`,
fullWidth: true,
disabled: isLoading,
isLoading,
},
}}
/>
);
};
CreateComponent.displayName = 'Create';
export const Create = React.memo(CreateComponent);

View file

@ -0,0 +1,475 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { FormTestComponent } from '../../../common/test_utils';
import { Edit } from './edit';
import { customFieldsMock, customFieldsConfigurationMock } from '../../../containers/mock';
import userEvent from '@testing-library/user-event';
import type { CaseCustomFieldNumber } from '../../../../common/types/domain';
import { POPULATED_WITH_DEFAULT } from '../translations';
describe('Edit ', () => {
const onSubmit = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
const customField = customFieldsMock[4] as CaseCustomFieldNumber;
const customFieldConfiguration = customFieldsConfigurationMock[4];
it('renders correctly', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Edit
customField={customField}
customFieldConfiguration={customFieldConfiguration}
onSubmit={onSubmit}
isLoading={false}
canUpdate={true}
/>
</FormTestComponent>
);
expect(await screen.findByTestId('case-number-custom-field-test_key_5')).toBeInTheDocument();
expect(
await screen.findByTestId('case-number-custom-field-edit-button-test_key_5')
).toBeInTheDocument();
expect(await screen.findByText(customFieldConfiguration.label)).toBeInTheDocument();
expect(await screen.findByText('1234')).toBeInTheDocument();
});
it('does not shows the edit button if the user does not have permissions', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Edit
customField={customField}
customFieldConfiguration={customFieldConfiguration}
onSubmit={onSubmit}
isLoading={false}
canUpdate={false}
/>
</FormTestComponent>
);
expect(
screen.queryByTestId('case-number-custom-field-edit-button-test_key_1')
).not.toBeInTheDocument();
});
it('does not shows the edit button when loading', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Edit
customField={customField}
customFieldConfiguration={customFieldConfiguration}
onSubmit={onSubmit}
isLoading={true}
canUpdate={true}
/>
</FormTestComponent>
);
expect(
screen.queryByTestId('case-number-custom-field-edit-button-test_key_1')
).not.toBeInTheDocument();
});
it('shows the loading spinner when loading', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Edit
customField={customField}
customFieldConfiguration={customFieldConfiguration}
onSubmit={onSubmit}
isLoading={true}
canUpdate={true}
/>
</FormTestComponent>
);
expect(
await screen.findByTestId('case-number-custom-field-loading-test_key_5')
).toBeInTheDocument();
});
it('shows the no value number if the custom field is undefined', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Edit
customFieldConfiguration={customFieldConfiguration}
onSubmit={onSubmit}
isLoading={false}
canUpdate={true}
/>
</FormTestComponent>
);
expect(await screen.findByText('No value is added')).toBeInTheDocument();
});
it('uses the required value correctly if a required field is empty', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Edit
customField={{ ...customField, value: null }}
customFieldConfiguration={customFieldConfiguration}
onSubmit={onSubmit}
isLoading={false}
canUpdate={true}
/>
</FormTestComponent>
);
expect(await screen.findByText('No value is added')).toBeInTheDocument();
await userEvent.click(
await screen.findByTestId('case-number-custom-field-edit-button-test_key_5')
);
expect(
await screen.findByTestId(
`case-number-custom-field-form-field-${customFieldConfiguration.key}`
)
).toHaveValue(customFieldConfiguration.defaultValue as number);
expect(
await screen.findByText('This field is populated with the default value.')
).toBeInTheDocument();
await userEvent.click(
await screen.findByTestId('case-number-custom-field-submit-button-test_key_5')
);
await waitFor(() => {
expect(onSubmit).toBeCalledWith({
...customField,
value: customFieldConfiguration.defaultValue,
});
});
});
it('does not show the value when the custom field is undefined', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Edit
customFieldConfiguration={customFieldConfiguration}
onSubmit={onSubmit}
isLoading={false}
canUpdate={true}
/>
</FormTestComponent>
);
expect(screen.queryByTestId('number-custom-field-view-test_key_5')).not.toBeInTheDocument();
});
it('does not show the value when the value is null', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Edit
customField={{ ...customField, value: null }}
customFieldConfiguration={customFieldConfiguration}
onSubmit={onSubmit}
isLoading={false}
canUpdate={true}
/>
</FormTestComponent>
);
expect(screen.queryByTestId('number-custom-field-view-test_key_5')).not.toBeInTheDocument();
});
it('does not show the form when the user does not have permissions', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Edit
customField={customField}
customFieldConfiguration={customFieldConfiguration}
onSubmit={onSubmit}
isLoading={false}
canUpdate={false}
/>
</FormTestComponent>
);
expect(
screen.queryByTestId('case-number-custom-field-form-field-test_key_5')
).not.toBeInTheDocument();
expect(
screen.queryByTestId('case-number-custom-field-submit-button-test_key_5')
).not.toBeInTheDocument();
expect(
screen.queryByTestId('case-number-custom-field-cancel-button-test_key_5')
).not.toBeInTheDocument();
});
it('calls onSubmit when changing value', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Edit
customField={customField}
customFieldConfiguration={customFieldConfiguration}
onSubmit={onSubmit}
isLoading={false}
canUpdate={true}
/>
</FormTestComponent>
);
await userEvent.click(
await screen.findByTestId('case-number-custom-field-edit-button-test_key_5')
);
await userEvent.click(
await screen.findByTestId('case-number-custom-field-form-field-test_key_5')
);
await userEvent.paste('12345');
expect(
await screen.findByTestId('case-number-custom-field-submit-button-test_key_5')
).not.toBeDisabled();
await userEvent.click(
await screen.findByTestId('case-number-custom-field-submit-button-test_key_5')
);
await waitFor(() => {
expect(onSubmit).toBeCalledWith({
...customField,
value: 123412345,
});
});
});
it('calls onSubmit with defaultValue if no initialValue exists', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Edit
customField={{
...customField,
value: null,
}}
customFieldConfiguration={customFieldConfiguration}
onSubmit={onSubmit}
isLoading={false}
canUpdate={true}
/>
</FormTestComponent>
);
await userEvent.click(
await screen.findByTestId('case-number-custom-field-edit-button-test_key_5')
);
expect(await screen.findByText(POPULATED_WITH_DEFAULT)).toBeInTheDocument();
expect(await screen.findByTestId('case-number-custom-field-form-field-test_key_5')).toHaveValue(
customFieldConfiguration.defaultValue as number
);
expect(
await screen.findByTestId('case-number-custom-field-submit-button-test_key_5')
).not.toBeDisabled();
await userEvent.click(
await screen.findByTestId('case-number-custom-field-submit-button-test_key_5')
);
await waitFor(() => {
expect(onSubmit).toBeCalledWith({
...customField,
value: customFieldConfiguration.defaultValue,
});
});
});
it('sets the value to null if the number field is empty', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Edit
customField={customField}
customFieldConfiguration={{ ...customFieldConfiguration, required: false }}
onSubmit={onSubmit}
isLoading={false}
canUpdate={true}
/>
</FormTestComponent>
);
await userEvent.click(
await screen.findByTestId('case-number-custom-field-edit-button-test_key_5')
);
await userEvent.clear(
await screen.findByTestId('case-number-custom-field-form-field-test_key_5')
);
expect(
await screen.findByTestId('case-number-custom-field-submit-button-test_key_5')
).not.toBeDisabled();
await userEvent.click(
await screen.findByTestId('case-number-custom-field-submit-button-test_key_5')
);
await waitFor(() => {
expect(onSubmit).toBeCalledWith({
...customField,
value: null,
});
});
});
it('hides the form when clicking the cancel button', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Edit
customField={customField}
customFieldConfiguration={customFieldConfiguration}
onSubmit={onSubmit}
isLoading={false}
canUpdate={true}
/>
</FormTestComponent>
);
await userEvent.click(
await screen.findByTestId('case-number-custom-field-edit-button-test_key_5')
);
expect(
await screen.findByTestId('case-number-custom-field-form-field-test_key_5')
).toBeInTheDocument();
await userEvent.click(
await screen.findByTestId('case-number-custom-field-cancel-button-test_key_5')
);
expect(
screen.queryByTestId('case-number-custom-field-form-field-test_key_5')
).not.toBeInTheDocument();
});
it('reset to initial value when canceling', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Edit
customField={customField}
customFieldConfiguration={customFieldConfiguration}
onSubmit={onSubmit}
isLoading={false}
canUpdate={true}
/>
</FormTestComponent>
);
await userEvent.click(
await screen.findByTestId('case-number-custom-field-edit-button-test_key_5')
);
await userEvent.click(
await screen.findByTestId('case-number-custom-field-form-field-test_key_5')
);
await userEvent.paste('321');
expect(
await screen.findByTestId('case-number-custom-field-submit-button-test_key_5')
).not.toBeDisabled();
await userEvent.click(
await screen.findByTestId('case-number-custom-field-cancel-button-test_key_5')
);
expect(
screen.queryByTestId('case-number-custom-field-form-field-test_key_5')
).not.toBeInTheDocument();
await userEvent.click(
await screen.findByTestId('case-number-custom-field-edit-button-test_key_5')
);
expect(await screen.findByTestId('case-number-custom-field-form-field-test_key_5')).toHaveValue(
1234
);
});
it('shows validation error if the field is required', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Edit
customField={customField}
customFieldConfiguration={customFieldConfiguration}
onSubmit={onSubmit}
isLoading={false}
canUpdate={true}
/>
</FormTestComponent>
);
await userEvent.click(
await screen.findByTestId('case-number-custom-field-edit-button-test_key_5')
);
await userEvent.clear(
await screen.findByTestId('case-number-custom-field-form-field-test_key_5')
);
expect(await screen.findByText('My test label 5 is required.')).toBeInTheDocument();
});
it('does not shows a validation error if the field is not required', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Edit
customField={customField}
customFieldConfiguration={{ ...customFieldConfiguration, required: false }}
onSubmit={onSubmit}
isLoading={false}
canUpdate={true}
/>
</FormTestComponent>
);
await userEvent.click(
await screen.findByTestId('case-number-custom-field-edit-button-test_key_5')
);
await userEvent.clear(
await screen.findByTestId('case-number-custom-field-form-field-test_key_5')
);
expect(
await screen.findByTestId('case-number-custom-field-submit-button-test_key_5')
).not.toBeDisabled();
expect(screen.queryByText('My test label 1 is required.')).not.toBeInTheDocument();
});
it('shows validation error if the number is too big', async () => {
render(
<FormTestComponent onSubmit={onSubmit}>
<Edit
customField={customField}
customFieldConfiguration={customFieldConfiguration}
onSubmit={onSubmit}
isLoading={false}
canUpdate={true}
/>
</FormTestComponent>
);
await userEvent.click(
await screen.findByTestId('case-number-custom-field-edit-button-test_key_5')
);
await userEvent.clear(
await screen.findByTestId('case-number-custom-field-form-field-test_key_5')
);
await userEvent.click(
await screen.findByTestId('case-number-custom-field-form-field-test_key_5')
);
await userEvent.paste(`${2 ** 53 + 1}`);
expect(
await screen.findByText(
'The value of the My test label 5 should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.'
)
).toBeInTheDocument();
});
});

View file

@ -0,0 +1,246 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useEffect, useState, useCallback } from 'react';
import {
EuiButton,
EuiButtonEmpty,
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiLoadingSpinner,
EuiText,
} from '@elastic/eui';
import type { FormHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import {
useForm,
UseField,
Form,
useFormData,
} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import { NumericField } from '@kbn/es-ui-shared-plugin/static/forms/components';
import type { CaseCustomFieldNumber } from '../../../../common/types/domain';
import { CustomFieldTypes } from '../../../../common/types/domain';
import type { CasesConfigurationUICustomField } from '../../../../common/ui';
import type { CustomFieldType } from '../types';
import { View } from './view';
import {
CANCEL,
EDIT_CUSTOM_FIELDS_ARIA_LABEL,
NO_CUSTOM_FIELD_SET,
SAVE,
POPULATED_WITH_DEFAULT,
} from '../translations';
import { getNumberFieldConfig } from './config';
const isEmpty = (value: number | null | undefined) => {
return value == null;
};
interface FormState {
value: number | null;
isValid?: boolean;
submit: FormHook<{ value: number | null }>['submit'];
}
interface FormWrapper {
initialValue: number | null;
isLoading: boolean;
customFieldConfiguration: CasesConfigurationUICustomField;
onChange: (state: FormState) => void;
}
const FormWrapperComponent: React.FC<FormWrapper> = ({
initialValue,
customFieldConfiguration,
isLoading,
onChange,
}) => {
const { form } = useForm<{ value: number | null }>({
defaultValue: {
value:
customFieldConfiguration?.defaultValue != null && isEmpty(initialValue)
? Number(customFieldConfiguration.defaultValue)
: initialValue,
},
});
const [{ value }] = useFormData({ form });
const { submit, isValid } = form;
const formFieldConfig = getNumberFieldConfig({
required: customFieldConfiguration.required,
label: customFieldConfiguration.label,
});
const populatedWithDefault =
value === customFieldConfiguration?.defaultValue && isEmpty(initialValue);
useEffect(() => {
onChange({
value,
isValid,
submit,
});
}, [isValid, onChange, submit, value]);
return (
<Form form={form}>
<UseField
path="value"
config={formFieldConfig}
component={NumericField}
helpText={populatedWithDefault && POPULATED_WITH_DEFAULT}
componentProps={{
euiFieldProps: {
fullWidth: true,
disabled: isLoading,
isLoading,
'data-test-subj': `case-number-custom-field-form-field-${customFieldConfiguration.key}`,
},
}}
/>
</Form>
);
};
FormWrapperComponent.displayName = 'FormWrapper';
const EditComponent: CustomFieldType<CaseCustomFieldNumber>['Edit'] = ({
customField,
customFieldConfiguration,
onSubmit,
isLoading,
canUpdate,
}) => {
const initialValue = customField?.value ?? null;
const [isEdit, setIsEdit] = useState(false);
const [formState, setFormState] = useState<FormState>({
isValid: undefined,
submit: async () => ({ isValid: false, data: { value: null } }),
value: initialValue,
});
const onEdit = useCallback(() => {
setIsEdit(true);
}, []);
const onCancel = useCallback(() => {
setIsEdit(false);
}, []);
const onSubmitCustomField = useCallback(async () => {
const { isValid, data } = await formState.submit();
if (isValid) {
onSubmit({
...customField,
key: customField?.key ?? customFieldConfiguration.key,
type: CustomFieldTypes.NUMBER,
value: data.value ? Number(data.value) : null,
});
}
setIsEdit(false);
}, [customField, customFieldConfiguration.key, formState, onSubmit]);
const title = customFieldConfiguration.label;
const isNumberFieldValid =
formState.isValid ||
(formState.value === customFieldConfiguration.defaultValue && isEmpty(initialValue));
const isCustomFieldValueDefined = !isEmpty(customField?.value);
return (
<>
<EuiFlexGroup
alignItems="center"
gutterSize="none"
justifyContent="spaceBetween"
responsive={false}
>
<EuiFlexItem grow={false}>
<EuiText>
<h4>{title}</h4>
</EuiText>
</EuiFlexItem>
{isLoading && (
<EuiLoadingSpinner
data-test-subj={`case-number-custom-field-loading-${customFieldConfiguration.key}`}
/>
)}
{!isLoading && canUpdate && (
<EuiFlexItem grow={false}>
<EuiButtonIcon
data-test-subj={`case-number-custom-field-edit-button-${customFieldConfiguration.key}`}
aria-label={EDIT_CUSTOM_FIELDS_ARIA_LABEL(title)}
iconType={'pencil'}
onClick={onEdit}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
<EuiHorizontalRule margin="xs" />
<EuiFlexGroup
gutterSize="m"
data-test-subj={`case-number-custom-field-${customFieldConfiguration.key}`}
direction="column"
>
{!isCustomFieldValueDefined && !isEdit && (
<p data-test-subj="no-number-custom-field-value">{NO_CUSTOM_FIELD_SET}</p>
)}
{!isEdit && isCustomFieldValueDefined && (
<EuiFlexItem>
<View customField={customField} />
</EuiFlexItem>
)}
{isEdit && canUpdate && (
<EuiFlexGroup gutterSize="m" direction="column">
<EuiFlexItem>
<FormWrapperComponent
initialValue={initialValue}
isLoading={isLoading}
onChange={setFormState}
customFieldConfiguration={customFieldConfiguration}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>
<EuiButton
color="success"
data-test-subj={`case-number-custom-field-submit-button-${customFieldConfiguration.key}`}
fill
iconType="save"
onClick={onSubmitCustomField}
size="s"
disabled={!isNumberFieldValid || isLoading}
>
{SAVE}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
data-test-subj={`case-number-custom-field-cancel-button-${customFieldConfiguration.key}`}
iconType="cross"
onClick={onCancel}
size="s"
>
{CANCEL}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
)}
</EuiFlexGroup>
</>
);
};
EditComponent.displayName = 'Edit';
export const Edit = React.memo(EditComponent);

View file

@ -0,0 +1,48 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { screen } from '@testing-library/react';
import { CustomFieldTypes } from '../../../../common/types/domain';
import type { AppMockRenderer } from '../../../common/mock';
import { createAppMockRenderer } from '../../../common/mock';
import { getEuiTableColumn } from './get_eui_table_column';
describe('getEuiTableColumn ', () => {
let appMockRender: AppMockRenderer;
beforeEach(() => {
appMockRender = createAppMockRenderer();
jest.clearAllMocks();
});
it('returns a name and a render function', async () => {
const label = 'MockLabel';
expect(getEuiTableColumn({ label })).toEqual({
name: label,
render: expect.any(Function),
width: '150px',
'data-test-subj': 'number-custom-field-column',
});
});
it('render function renders a number column correctly', async () => {
const key = 'test_key_1';
const value = 1234567;
const column = getEuiTableColumn({ label: 'MockLabel' });
appMockRender.render(<div>{column.render({ key, type: CustomFieldTypes.NUMBER, value })}</div>);
expect(screen.getByTestId(`number-custom-field-column-view-${key}`)).toBeInTheDocument();
expect(screen.getByTestId(`number-custom-field-column-view-${key}`)).toHaveTextContent(
String(value)
);
});
});

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import type { CaseCustomField } from '../../../../common/types/domain';
import type { CustomFieldEuiTableColumn } from '../types';
export const getEuiTableColumn = ({ label }: { label: string }): CustomFieldEuiTableColumn => ({
name: label,
width: '150px',
render: (customField: CaseCustomField) => {
return (
<p
className="eui-textNumber"
data-test-subj={`number-custom-field-column-view-${customField.key}`}
>
{customField.value}
</p>
);
},
'data-test-subj': 'number-custom-field-column',
});

View file

@ -0,0 +1,29 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { render, screen } from '@testing-library/react';
import { CustomFieldTypes } from '../../../../common/types/domain';
import { View } from './view';
describe('View ', () => {
beforeEach(() => {
jest.clearAllMocks();
});
const customField = {
type: CustomFieldTypes.NUMBER as const,
key: 'test_key_1',
value: 123 as number,
};
it('renders correctly', async () => {
render(<View customField={customField} />);
expect(screen.getByText('123')).toBeInTheDocument();
});
});

View file

@ -0,0 +1,29 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiText } from '@elastic/eui';
import type { CaseCustomFieldNumber } from '../../../../common/types/domain';
import type { CustomFieldType } from '../types';
const ViewComponent: CustomFieldType<CaseCustomFieldNumber>['View'] = ({ customField }) => {
const value = customField?.value ?? '-';
return (
<EuiText
className="eui-textNumber"
data-test-subj={`text-custom-field-view-${customField?.key}`}
>
{value}
</EuiText>
);
};
ViewComponent.displayName = 'View';
export const View = React.memo(ViewComponent);

View file

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

View file

@ -51,6 +51,10 @@ export const TOGGLE_LABEL = i18n.translate('xpack.cases.customFields.toggleLabel
defaultMessage: 'Toggle',
});
export const NUMBER_LABEL = i18n.translate('xpack.cases.customFields.textLabel', {
defaultMessage: 'Number',
});
export const FIELD_TYPE = i18n.translate('xpack.cases.customFields.fieldType', {
defaultMessage: 'Field type',
});

View file

@ -55,7 +55,7 @@ export type CustomFieldFactory<T extends CaseUICustomField> = () => {
build: () => CustomFieldType<T>;
filterOptions?: CustomFieldFactoryFilterOption[];
getDefaultValue?: () => string | boolean | null;
convertNullToEmpty?: (value: string | boolean | null) => string;
convertNullToEmpty?: (value: string | number | boolean | null) => string;
};
export type CustomFieldBuilderMap = {

View file

@ -97,5 +97,40 @@ describe('utils ', () => {
}
`);
});
it('serializes the data correctly if the default value is integer number', async () => {
const customField = {
key: 'my_test_key',
type: CustomFieldTypes.NUMBER,
required: true,
defaultValue: 1,
} as CustomFieldConfiguration;
expect(customFieldSerializer(customField)).toMatchInlineSnapshot(`
Object {
"defaultValue": 1,
"key": "my_test_key",
"required": true,
"type": "number",
}
`);
});
it('serializes the data correctly if the default value is float number', async () => {
const customField = {
key: 'my_test_key',
type: CustomFieldTypes.NUMBER,
required: true,
defaultValue: 1.5,
} as CustomFieldConfiguration;
expect(customFieldSerializer(customField)).toMatchInlineSnapshot(`
Object {
"key": "my_test_key",
"required": true,
"type": "number",
}
`);
});
});
});

View file

@ -8,6 +8,7 @@
import { isEmptyString } from '@kbn/es-ui-shared-plugin/static/validators/string';
import { isString } from 'lodash';
import type { CustomFieldConfiguration } from '../../../common/types/domain';
import { CustomFieldTypes } from '../../../common/types/domain';
export const customFieldSerializer = (
field: CustomFieldConfiguration
@ -18,5 +19,13 @@ export const customFieldSerializer = (
return otherProperties;
}
if (field.type === CustomFieldTypes.NUMBER) {
if (defaultValue !== null && Number.isSafeInteger(Number(defaultValue))) {
return { ...field, defaultValue: Number(defaultValue) };
} else {
return otherProperties;
}
}
return field;
};

View file

@ -589,11 +589,14 @@ describe('TemplateForm', () => {
expect(
await within(customFieldsElement).findAllByTestId('form-optional-field-label')
).toHaveLength(
customFieldsConfigurationMock.filter((field) => field.type === CustomFieldTypes.TEXT).length
customFieldsConfigurationMock.filter(
(field) => field.type === CustomFieldTypes.TEXT || field.type === CustomFieldTypes.NUMBER
).length
);
const textField = customFieldsConfigurationMock[0];
const toggleField = customFieldsConfigurationMock[3];
const numberField = customFieldsConfigurationMock[4];
const textCustomField = await screen.findByTestId(
`${textField.key}-${textField.type}-create-custom-field`
@ -608,6 +611,15 @@ describe('TemplateForm', () => {
await screen.findByTestId(`${toggleField.key}-${toggleField.type}-create-custom-field`)
);
const numberCustomField = await screen.findByTestId(
`${numberField.key}-${numberField.type}-create-custom-field`
);
await user.clear(numberCustomField);
await user.click(numberCustomField);
await user.paste('765');
const submitSpy = jest.spyOn(formState!, 'submit');
await user.click(screen.getByText('testSubmit'));
@ -644,6 +656,16 @@ describe('TemplateForm', () => {
type: 'toggle',
value: true,
},
{
key: 'test_key_5',
type: 'number',
value: 1234,
},
{
key: 'test_key_6',
type: 'number',
value: true,
},
],
settings: {
syncAlerts: true,

View file

@ -311,6 +311,7 @@ describe('form fields', () => {
const textField = customFieldsConfigurationMock[0];
const toggleField = customFieldsConfigurationMock[1];
const numberField = customFieldsConfigurationMock[4];
const textCustomField = await screen.findByTestId(
`${textField.key}-${textField.type}-create-custom-field`
@ -324,6 +325,14 @@ describe('form fields', () => {
await screen.findByTestId(`${toggleField.key}-${toggleField.type}-create-custom-field`)
);
const numberCustomField = await screen.findByTestId(
`${numberField.key}-${numberField.type}-create-custom-field`
);
await userEvent.clear(numberCustomField);
await userEvent.click(numberCustomField);
await userEvent.paste('987');
await userEvent.click(screen.getByText('Submit'));
await waitFor(() => {
@ -336,6 +345,7 @@ describe('form fields', () => {
test_key_1: 'My text test value 1',
test_key_2: false,
test_key_4: false,
test_key_5: '987',
},
syncAlerts: true,
templateTags: [],

View file

@ -523,19 +523,46 @@ describe('Utils', () => {
});
it('returns the string when the value is a non-empty string', async () => {
expect(convertCustomFieldValue('my text value')).toMatchInlineSnapshot(`"my text value"`);
expect(
convertCustomFieldValue({ value: 'my text value', type: CustomFieldTypes.TEXT })
).toMatchInlineSnapshot(`"my text value"`);
});
it('returns null when value is empty string', async () => {
expect(convertCustomFieldValue('')).toMatchInlineSnapshot('null');
expect(
convertCustomFieldValue({ value: '', type: CustomFieldTypes.TEXT })
).toMatchInlineSnapshot('null');
});
it('returns value as it is when value is true', async () => {
expect(convertCustomFieldValue(true)).toMatchInlineSnapshot('true');
expect(
convertCustomFieldValue({ value: true, type: CustomFieldTypes.TOGGLE })
).toMatchInlineSnapshot('true');
});
it('returns value as it is when value is false', async () => {
expect(convertCustomFieldValue(false)).toMatchInlineSnapshot('false');
expect(
convertCustomFieldValue({ value: false, type: CustomFieldTypes.TOGGLE })
).toMatchInlineSnapshot('false');
});
it('returns value as integer number when value is integer string and type is number', () => {
expect(convertCustomFieldValue({ value: '123', type: CustomFieldTypes.NUMBER })).toEqual(123);
});
it('returns value as null when value is float string and type is number', () => {
expect(convertCustomFieldValue({ value: '0.5', type: CustomFieldTypes.NUMBER })).toEqual(
null
);
});
it('returns value as null when value is null and type is number', () => {
expect(convertCustomFieldValue({ value: null, type: CustomFieldTypes.NUMBER })).toEqual(null);
});
it('returns value as null when value is characters string and type is number', () => {
expect(convertCustomFieldValue({ value: 'fdgdg', type: CustomFieldTypes.NUMBER })).toEqual(
null
);
});
});
@ -575,6 +602,16 @@ describe('Utils', () => {
"type": "toggle",
"value": null,
},
Object {
"key": "test_key_5",
"type": "number",
"value": 1234,
},
Object {
"key": "test_key_6",
"type": "number",
"value": null,
},
Object {
"key": "my_test_key",
"type": "text",
@ -598,6 +635,8 @@ describe('Utils', () => {
{ ...customFieldsMock[1] },
{ ...customFieldsMock[2] },
{ ...customFieldsMock[3] },
{ ...customFieldsMock[4] },
{ ...customFieldsMock[5] },
],
`
Array [
@ -626,6 +665,16 @@ describe('Utils', () => {
"type": "toggle",
"value": null,
},
Object {
"key": "test_key_5",
"type": "number",
"value": 1234,
},
Object {
"key": "test_key_6",
"type": "number",
"value": null,
},
]
`
);
@ -669,6 +718,19 @@ describe('Utils', () => {
"required": false,
"type": "toggle",
},
Object {
"defaultValue": 123,
"key": "test_key_5",
"label": "My test label 5",
"required": true,
"type": "number",
},
Object {
"key": "test_key_6",
"label": "My test label 6",
"required": false,
"type": "number",
},
Object {
"key": "my_test_key",
"label": "my_test_label",
@ -693,6 +755,8 @@ describe('Utils', () => {
{ ...customFieldsConfigurationMock[1] },
{ ...customFieldsConfigurationMock[2] },
{ ...customFieldsConfigurationMock[3] },
{ ...customFieldsConfigurationMock[4] },
{ ...customFieldsConfigurationMock[5] },
],
`
Array [
@ -722,6 +786,19 @@ describe('Utils', () => {
"required": false,
"type": "toggle",
},
Object {
"defaultValue": 123,
"key": "test_key_5",
"label": "My test label 5",
"required": true,
"type": "number",
},
Object {
"key": "test_key_6",
"label": "My test label 6",
"required": false,
"type": "number",
},
]
`
);

View file

@ -13,7 +13,7 @@ import type {
} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import type { UserProfileWithAvatar } from '@kbn/user-profile-components';
import type { ConnectorTypeFields } from '../../common/types/domain';
import { ConnectorTypes } from '../../common/types/domain';
import { ConnectorTypes, CustomFieldTypes } from '../../common/types/domain';
import type { CasesPublicStartDependencies } from '../types';
import { connectorValidator as swimlaneConnectorValidator } from './connectors/swimlane/validator';
import type { CaseActionConnector } from './types';
@ -234,11 +234,25 @@ export const parseCaseUsers = ({
return { userProfiles, reporterAsArray };
};
export const convertCustomFieldValue = (value: string | boolean) => {
export const convertCustomFieldValue = ({
value,
type,
}: {
value: string | number | boolean | null;
type: CustomFieldTypes;
}) => {
if (typeof value === 'string' && isEmpty(value)) {
return null;
}
if (type === CustomFieldTypes.NUMBER) {
if (value !== null && Number.isSafeInteger(Number(value))) {
return Number(value);
} else {
return null;
}
}
return value;
};
@ -288,7 +302,7 @@ export const customFieldsFormDeserializer = (
};
export const customFieldsFormSerializer = (
customFields: Record<string, string | boolean>,
customFields: Record<string, string | boolean | number | null>,
selectedCustomFieldsConfiguration: CasesConfigurationUI['customFields']
): CaseUI['customFields'] => {
const transformedCustomFields: CaseUI['customFields'] = [];
@ -303,7 +317,7 @@ export const customFieldsFormSerializer = (
transformedCustomFields.push({
key: configCustomField.key,
type: configCustomField.type,
value: convertCustomFieldValue(value),
value: convertCustomFieldValue({ value, type: configCustomField.type }),
} as CaseUICustomField);
}
}

View file

@ -1158,6 +1158,8 @@ export const customFieldsMock: CaseUICustomField[] = [
{ type: CustomFieldTypes.TOGGLE, key: 'test_key_2', value: true },
{ type: CustomFieldTypes.TEXT, key: 'test_key_3', value: null },
{ type: CustomFieldTypes.TOGGLE, key: 'test_key_4', value: null },
{ type: CustomFieldTypes.NUMBER, key: 'test_key_5', value: 1234 },
{ type: CustomFieldTypes.NUMBER, key: 'test_key_6', value: null },
];
export const customFieldsConfigurationMock: CasesConfigurationUICustomField[] = [
@ -1177,6 +1179,19 @@ export const customFieldsConfigurationMock: CasesConfigurationUICustomField[] =
},
{ type: CustomFieldTypes.TEXT, key: 'test_key_3', label: 'My test label 3', required: false },
{ type: CustomFieldTypes.TOGGLE, key: 'test_key_4', label: 'My test label 4', required: false },
{
type: CustomFieldTypes.NUMBER,
key: 'test_key_5',
label: 'My test label 5',
required: true,
defaultValue: 123,
},
{
type: CustomFieldTypes.NUMBER,
key: 'test_key_6',
label: 'My test label 6',
required: false,
},
];
export const templatesConfigurationMock: CasesConfigurationUITemplate[] = [

View file

@ -16,7 +16,7 @@ import * as i18n from './translations';
interface ReplaceCustomField {
caseId: string;
customFieldId: string;
customFieldValue: string | boolean | null;
customFieldValue: string | number | boolean | null;
caseVersion: string;
}

View file

@ -906,7 +906,7 @@ describe('utils', () => {
...customFieldsConfiguration,
{
key: 'fourth_key',
type: 'number',
type: 'symbol',
label: 'Number field',
required: true,
},

View file

@ -39,7 +39,7 @@ type PersistedCustomFieldsConfiguration = Array<{
type: string;
label: string;
required: boolean;
defaultValue?: string | boolean | null;
defaultValue?: string | number | boolean | null;
}>;
type PersistedTemplatesConfiguration = Array<{

View file

@ -12,8 +12,11 @@ export const MAX_OPEN_CASES = 10;
export const DEFAULT_MAX_OPEN_CASES = 5;
export const INITIAL_ORACLE_RECORD_COUNTER = 1;
export const VALUES_FOR_CUSTOM_FIELDS_MISSING_DEFAULTS: Record<CustomFieldTypes, string | boolean> =
{
[CustomFieldTypes.TEXT]: 'N/A',
[CustomFieldTypes.TOGGLE]: false,
};
export const VALUES_FOR_CUSTOM_FIELDS_MISSING_DEFAULTS: Record<
CustomFieldTypes,
string | boolean | number
> = {
[CustomFieldTypes.TEXT]: 'N/A',
[CustomFieldTypes.TOGGLE]: false,
[CustomFieldTypes.NUMBER]: 0,
};

View file

@ -9,10 +9,12 @@ import { CustomFieldTypes } from '../../common/types/domain';
import type { ICasesCustomField, CasesCustomFieldsMap } from './types';
import { getCasesTextCustomField } from './text';
import { getCasesToggleCustomField } from './toggle';
import { getCasesNumberCustomField } from './number';
const mapping: Record<CustomFieldTypes, ICasesCustomField> = {
[CustomFieldTypes.TEXT]: getCasesTextCustomField(),
[CustomFieldTypes.TOGGLE]: getCasesToggleCustomField(),
[CustomFieldTypes.NUMBER]: getCasesNumberCustomField(),
};
export const casesCustomFields: CasesCustomFieldsMap = {

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import Boom from '@hapi/boom';
export const getCasesNumberCustomField = () => ({
isFilterable: false,
isSortable: false,
savedObjectMappingType: 'long',
validateFilteringValues: (values: Array<string | number | boolean | null>) => {
values.forEach((value) => {
if (value !== null && !Number.isSafeInteger(value)) {
throw Boom.badRequest('Unsupported filtering value for custom field of type number.');
}
});
},
});

View file

@ -334,6 +334,13 @@ export default ({ getService }: FtrProviderContext): void => {
defaultValue: false,
required: true,
},
{
key: 'test_custom_field_3',
label: 'toggle',
type: CustomFieldTypes.NUMBER,
defaultValue: 1,
required: true,
},
],
},
})
@ -367,6 +374,11 @@ export default ({ getService }: FtrProviderContext): void => {
type: CustomFieldTypes.TOGGLE,
value: true,
},
{
key: 'test_custom_field_3',
type: CustomFieldTypes.NUMBER,
value: 2,
},
],
},
],
@ -384,6 +396,11 @@ export default ({ getService }: FtrProviderContext): void => {
type: CustomFieldTypes.TOGGLE,
value: true,
},
{
key: 'test_custom_field_3',
type: CustomFieldTypes.NUMBER,
value: 2,
},
]);
});
@ -406,6 +423,12 @@ export default ({ getService }: FtrProviderContext): void => {
defaultValue: false,
required: true,
},
{
key: 'test_custom_field_3',
label: 'number',
type: CustomFieldTypes.NUMBER,
required: false,
},
],
},
})
@ -444,6 +467,7 @@ export default ({ getService }: FtrProviderContext): void => {
expect(patchedCases[0].customFields).to.eql([
{ key: 'test_custom_field_2', type: 'toggle', value: true },
{ key: 'test_custom_field_1', type: 'text', value: null },
{ key: 'test_custom_field_3', type: 'number', value: null },
]);
});
@ -1106,6 +1130,13 @@ export default ({ getService }: FtrProviderContext): void => {
defaultValue: false,
required: true,
},
{
key: 'number_custom_field',
label: 'number',
type: CustomFieldTypes.NUMBER,
defaultValue: 3,
required: true,
},
],
},
})
@ -1122,6 +1153,11 @@ export default ({ getService }: FtrProviderContext): void => {
type: CustomFieldTypes.TOGGLE,
value: true,
},
{
key: 'number_custom_field',
type: CustomFieldTypes.NUMBER,
value: 4,
},
] as CaseCustomFields;
const postedCase = await createCase(supertest, {
@ -1145,6 +1181,7 @@ export default ({ getService }: FtrProviderContext): void => {
expect(patchedCases[0].customFields).to.eql([
{ ...originalValues[0], value: 'default value' },
{ ...originalValues[1], value: false },
{ ...originalValues[2], value: 3 },
]);
});
@ -1168,6 +1205,13 @@ export default ({ getService }: FtrProviderContext): void => {
defaultValue: false,
required: false,
},
{
key: 'number_custom_field',
label: 'number',
type: CustomFieldTypes.NUMBER,
defaultValue: 5,
required: false,
},
],
},
})
@ -1184,6 +1228,11 @@ export default ({ getService }: FtrProviderContext): void => {
type: CustomFieldTypes.TOGGLE,
value: true,
},
{
key: 'number_custom_field',
type: CustomFieldTypes.NUMBER,
value: 6,
},
] as CaseCustomFields;
const postedCase = await createCase(supertest, {
@ -1213,6 +1262,7 @@ export default ({ getService }: FtrProviderContext): void => {
expect(patchedCases[0].customFields).to.eql([
{ ...originalValues[1], value: false },
{ ...originalValues[0], value: 'default value' },
{ ...originalValues[2], value: 5 },
]);
});
@ -1234,6 +1284,12 @@ export default ({ getService }: FtrProviderContext): void => {
type: CustomFieldTypes.TOGGLE,
required: true,
},
{
key: 'number_custom_field',
label: 'number',
type: CustomFieldTypes.NUMBER,
required: true,
},
],
},
})
@ -1252,6 +1308,11 @@ export default ({ getService }: FtrProviderContext): void => {
type: CustomFieldTypes.TOGGLE,
value: true,
},
{
key: 'number_custom_field',
type: CustomFieldTypes.NUMBER,
value: 7,
},
],
});
@ -1358,6 +1419,13 @@ export default ({ getService }: FtrProviderContext): void => {
required: true,
defaultValue: false,
},
{
key: 'number_custom_field',
label: 'number',
type: CustomFieldTypes.NUMBER,
required: true,
defaultValue: 8,
},
],
},
})
@ -1376,6 +1444,11 @@ export default ({ getService }: FtrProviderContext): void => {
type: CustomFieldTypes.TOGGLE,
value: true,
},
{
key: 'number_custom_field',
type: CustomFieldTypes.NUMBER,
value: 9,
},
],
});
@ -1390,6 +1463,11 @@ export default ({ getService }: FtrProviderContext): void => {
type: CustomFieldTypes.TOGGLE,
value: null,
},
{
key: 'number_custom_field',
type: CustomFieldTypes.NUMBER,
value: null,
},
];
await updateCase({

View file

@ -192,6 +192,13 @@ export default ({ getService }: FtrProviderContext): void => {
defaultValue: false,
required: true,
},
{
key: 'valid_key_3',
label: 'number',
type: CustomFieldTypes.NUMBER,
defaultValue: 123,
required: true,
},
],
},
})
@ -211,6 +218,11 @@ export default ({ getService }: FtrProviderContext): void => {
type: CustomFieldTypes.TOGGLE,
value: true,
},
{
key: 'valid_key_3',
type: CustomFieldTypes.NUMBER,
value: 123456,
},
],
})
);
@ -226,6 +238,11 @@ export default ({ getService }: FtrProviderContext): void => {
type: CustomFieldTypes.TOGGLE,
value: true,
},
{
key: 'valid_key_3',
type: CustomFieldTypes.NUMBER,
value: 123456,
},
]);
});
@ -248,6 +265,13 @@ export default ({ getService }: FtrProviderContext): void => {
defaultValue: false,
required: true,
},
{
key: 'valid_key_3',
label: 'number',
type: CustomFieldTypes.NUMBER,
defaultValue: 123,
required: false,
},
],
},
})
@ -269,6 +293,7 @@ export default ({ getService }: FtrProviderContext): void => {
expect(res.customFields).to.eql([
{ key: 'valid_key_2', type: 'toggle', value: true },
{ key: 'valid_key_1', type: 'text', value: null },
{ key: 'valid_key_3', type: 'number', value: 123 },
]);
});
@ -278,8 +303,8 @@ export default ({ getService }: FtrProviderContext): void => {
key: 'text_custom_field',
label: 'text',
type: CustomFieldTypes.TEXT,
required: true,
defaultValue: 'default value',
required: true,
},
{
key: 'toggle_custom_field',
@ -288,6 +313,13 @@ export default ({ getService }: FtrProviderContext): void => {
defaultValue: false,
required: true,
},
{
key: 'number_custom_field',
label: 'number',
type: CustomFieldTypes.NUMBER,
defaultValue: 123,
required: true,
},
];
await createConfiguration(
@ -316,6 +348,11 @@ export default ({ getService }: FtrProviderContext): void => {
type: customFieldsConfiguration[1].type,
value: false,
},
{
key: customFieldsConfiguration[2].key,
type: customFieldsConfiguration[2].type,
value: 123,
},
]);
});
@ -335,6 +372,13 @@ export default ({ getService }: FtrProviderContext): void => {
defaultValue: false,
required: false,
},
{
key: 'number_custom_field',
label: 'number',
type: CustomFieldTypes.NUMBER,
defaultValue: 123,
required: false,
},
];
await createConfiguration(
@ -363,6 +407,11 @@ export default ({ getService }: FtrProviderContext): void => {
type: customFieldsConfiguration[1].type,
value: false,
},
{
key: customFieldsConfiguration[2].key,
type: customFieldsConfiguration[2].type,
value: 123,
},
]);
});
});
@ -594,6 +643,13 @@ export default ({ getService }: FtrProviderContext): void => {
defaultValue: false,
required: true,
},
{
key: 'number_custom_field',
label: 'number',
type: CustomFieldTypes.NUMBER,
defaultValue: 123,
required: true,
},
];
await createConfiguration(
@ -619,6 +675,11 @@ export default ({ getService }: FtrProviderContext): void => {
type: CustomFieldTypes.TOGGLE,
value: null,
},
{
key: 'number_custom_field',
type: CustomFieldTypes.NUMBER,
value: null,
},
],
}),
400
@ -642,6 +703,7 @@ export default ({ getService }: FtrProviderContext): void => {
},
})
);
await createCase(
supertest,
getPostCaseRequest({

View file

@ -70,6 +70,13 @@ export default ({ getService }: FtrProviderContext): void => {
required: true,
defaultValue: false,
},
{
key: 'num',
label: 'number',
type: CustomFieldTypes.NUMBER,
required: true,
defaultValue: 1,
},
],
};
await createConfiguration(

View file

@ -268,6 +268,12 @@ export default ({ getService }: FtrProviderContext): void => {
type: CustomFieldTypes.TOGGLE,
required: false,
},
{
key: 'number_field_1',
label: 'Number field 1',
type: CustomFieldTypes.NUMBER,
required: false,
},
];
const templates = [
@ -293,6 +299,11 @@ export default ({ getService }: FtrProviderContext): void => {
value: true,
type: CustomFieldTypes.TOGGLE,
},
{
key: 'number_field_1',
value: 123,
type: CustomFieldTypes.NUMBER,
},
],
connector: {
id: 'none',

View file

@ -88,6 +88,19 @@ export default ({ getService }: FtrProviderContext): void => {
required: false,
defaultValue: true,
},
{
key: 'number_1',
label: 'number 1',
type: CustomFieldTypes.NUMBER,
required: false,
},
{
key: 'number_2',
label: 'number 2',
type: CustomFieldTypes.NUMBER,
required: true,
defaultValue: 2,
},
],
};
@ -116,6 +129,12 @@ export default ({ getService }: FtrProviderContext): void => {
type: CustomFieldTypes.TOGGLE,
required: false,
},
{
key: 'number_field_1',
label: '#3',
type: CustomFieldTypes.NUMBER,
required: false,
},
];
const templates = [
@ -135,6 +154,11 @@ export default ({ getService }: FtrProviderContext): void => {
value: false,
type: CustomFieldTypes.TOGGLE,
},
{
key: 'number_field_1',
value: 3,
type: CustomFieldTypes.NUMBER,
},
],
},
},
@ -161,6 +185,11 @@ export default ({ getService }: FtrProviderContext): void => {
value: true,
type: CustomFieldTypes.TOGGLE,
},
{
key: 'number_field_1',
value: 4,
type: CustomFieldTypes.NUMBER,
},
],
connector: {
id: 'none',
@ -189,6 +218,11 @@ export default ({ getService }: FtrProviderContext): void => {
value: false,
type: CustomFieldTypes.TOGGLE,
},
{
key: 'number_field_1',
value: 5,
type: CustomFieldTypes.NUMBER,
},
],
},
},

View file

@ -381,6 +381,12 @@ export default ({ getService }: FtrProviderContext): void => {
type: CustomFieldTypes.TEXT,
required: false,
},
{
key: 'number_custom_field_4',
label: 'number',
type: CustomFieldTypes.NUMBER,
required: false,
},
],
},
})
@ -402,6 +408,11 @@ export default ({ getService }: FtrProviderContext): void => {
type: CustomFieldTypes.TEXT,
value: 'this is a text field value 3',
},
{
key: 'number_custom_field_4',
type: CustomFieldTypes.NUMBER,
value: 123,
},
];
const theCase = await createCase(supertest, {

View file

@ -1241,6 +1241,13 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
defaultValue: false,
required: true,
},
{
key: 'valid_key_3',
label: 'Sync',
type: CustomFieldTypes.NUMBER as const,
defaultValue: 123,
required: true,
},
];
before(async () => {
@ -1258,6 +1265,11 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
type: CustomFieldTypes.TOGGLE,
value: true,
},
{
key: 'valid_key_3',
type: CustomFieldTypes.NUMBER,
value: 1234,
},
],
});
await cases.casesTable.waitForCasesToBeListed();
@ -1311,6 +1323,33 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
expect(userActions).length(2);
});
it('updates a number custom field correctly', async () => {
const numberField = await testSubjects.find(
`case-number-custom-field-${customFields[2].key}`
);
expect(await numberField.getVisibleText()).equal('1234');
await testSubjects.click(`case-number-custom-field-edit-button-${customFields[2].key}`);
await retry.waitFor('custom field edit form to exist', async () => {
return await testSubjects.exists(
`case-number-custom-field-form-field-${customFields[2].key}`
);
});
const inputField = await testSubjects.find(
`case-number-custom-field-form-field-${customFields[2].key}`
);
await inputField.type('12345');
await testSubjects.click(`case-number-custom-field-submit-button-${customFields[2].key}`);
await header.waitUntilLoadingHasFinished();
expect(await numberField.getVisibleText()).equal('123412345');
});
});
});
};

View file

@ -126,6 +126,58 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
await testSubjects.missingOrFail('custom-fields-list');
});
it('adds a number custom field', async () => {
await testSubjects.existOrFail('custom-fields-form-group');
await common.clickAndValidate('add-custom-field', 'common-flyout');
await testSubjects.setValue('custom-field-label-input', 'Count');
await testSubjects.click('custom-field-type-selector');
await (await find.byCssSelector('[value="number"]')).click();
await testSubjects.setCheckbox('number-custom-field-required-wrapper', 'check');
const defaultNumberInput = await testSubjects.find('number-custom-field-default-value');
await defaultNumberInput.type('0');
await testSubjects.click('common-flyout-save');
expect(await testSubjects.exists('euiFlyoutCloseButton')).to.be(false);
await testSubjects.existOrFail('custom-fields-list');
expect(await testSubjects.getVisibleText('custom-fields-list')).to.be('Count\nNumber');
});
it('edits a number custom field', async () => {
await testSubjects.existOrFail('custom-fields-form-group');
const numberField = await find.byCssSelector('[data-test-subj*="-custom-field-edit"]');
await numberField.click();
const labelInput = await testSubjects.find('custom-field-label-input');
await labelInput.type('!');
await testSubjects.setValue('number-custom-field-default-value', '321');
await testSubjects.click('common-flyout-save');
expect(await testSubjects.exists('euiFlyoutCloseButton')).to.be(false);
await testSubjects.existOrFail('custom-fields-list');
expect(await testSubjects.getVisibleText('custom-fields-list')).to.be('Count!\nNumber');
});
it('deletes a number custom field', async () => {
await testSubjects.existOrFail('custom-fields-form-group');
const deleteButton = await find.byCssSelector('[data-test-subj*="-custom-field-delete"]');
await deleteButton.click();
await testSubjects.existOrFail('confirm-delete-modal');
await testSubjects.click('confirmModalConfirmButton');
await testSubjects.missingOrFail('custom-fields-list');
});
});
describe('Templates', function () {