mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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>

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:
parent
5576316aba
commit
7cad9c31f6
59 changed files with 2474 additions and 41 deletions
|
@ -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.",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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) };
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
|
@ -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),
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
}),
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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);
|
|
@ -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)
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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',
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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);
|
|
@ -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),
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
]
|
||||
`
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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[] = [
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -906,7 +906,7 @@ describe('utils', () => {
|
|||
...customFieldsConfiguration,
|
||||
{
|
||||
key: 'fourth_key',
|
||||
type: 'number',
|
||||
type: 'symbol',
|
||||
label: 'Number field',
|
||||
required: true,
|
||||
},
|
||||
|
|
|
@ -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<{
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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 = {
|
||||
|
|
21
x-pack/plugins/cases/server/custom_fields/number.ts
Normal file
21
x-pack/plugins/cases/server/custom_fields/number.ts
Normal 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.');
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
|
@ -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({
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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 () {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue