Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d774142
[Cases] Add renderInAllCases flag to reusable field definitions
lgestc May 19, 2026
3d0b5d8
[Cases] Allow global fields when clearing a template
lgestc May 19, 2026
68456c5
[Cases] Show global fields in create form when no template is selected
lgestc May 19, 2026
2045598
[Cases] Rename renderInAllCases to applyToAllCases
lgestc May 20, 2026
5d63116
[Cases] Fix global fields not saved on create and rename label to apply
lgestc May 20, 2026
04a4ae4
[Cases] Resolve merge conflicts with main
lgestc May 20, 2026
1e558b0
[Cases] Code review fixes: shared YAML util, N+1 fix, SO model versio…
lgestc May 20, 2026
03b4f55
Merge branch 'main' into tasks/cases-global-reusable-fields
lgestc May 20, 2026
53f81e1
[Cases] Fix missing GlobalCaseFields mock in case_view_activity test
lgestc May 21, 2026
a9ea459
[Cases] Address code review findings on applyToAllCases PR
lgestc May 21, 2026
479a070
[Cases] Filter global fields that are already referenced via \$ref in…
lgestc May 21, 2026
6e2f45d
Changes from node scripts/check
kibanamachine May 21, 2026
59cdfd3
Merge branch 'main' into tasks/cases-global-reusable-fields
lgestc May 21, 2026
fbf930e
Resolve merge conflict in template_fields.tsx
lgestc May 22, 2026
d9ee76f
Merge branch 'main' into tasks/cases-global-reusable-fields
lgestc May 22, 2026
12d93eb
Merge branch 'main' into tasks/cases-global-reusable-fields
lgestc May 25, 2026
329746e
Merge branch 'main' into tasks/cases-global-reusable-fields
lgestc May 25, 2026
d4342de
[Cases] fix: render global fields without template selected; add Exte…
lgestc May 26, 2026
c1d70bc
Merge branch 'main' into tasks/cases-global-reusable-fields
lgestc May 26, 2026
acb0df5
[Cases] fix: move extended_fields merge server-side; address review f…
lgestc May 26, 2026
bc15865
[Cases] fix: add integration tests and address review comments
lgestc May 26, 2026
1a0d6b4
Merge branch 'main' into tasks/cases-global-reusable-fields
lgestc May 26, 2026
7d74f7d
[Cases] Avoid inline empty object literals to prevent unnecessary re-…
lgestc May 27, 2026
98a0d01
[Cases] Move global fields above custom fields, remove section title,…
lgestc May 27, 2026
b224f52
[Cases] Fix Jest test failures after template_fields and validator ch…
lgestc May 27, 2026
05b166e
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine May 27, 2026
7ea2e3e
Merge branch 'main' into tasks/cases-global-reusable-fields
lgestc May 28, 2026
baa5d44
Changes from node scripts/check
kibanamachine May 28, 2026
5f0e472
[Cases] Fix missing EuiToolTip import in all_field_definitions_page
lgestc May 28, 2026
9f14620
[Cases] Render global fields under Extended fields heading in case view
lgestc May 28, 2026
9a94067
[Cases] Remove extra gap between template and global fields in case v…
lgestc May 28, 2026
eb743d5
Merge branch 'main' into tasks/cases-global-reusable-fields
lgestc May 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import { FieldDefinitionSchema } from '../../domain/field_definition/v1';

export const FieldDefinitionsFindRequestSchema = z.object({
owner: z.union([Owner, Owners]).optional(),
// This schema describes the TypeScript/client-side type (boolean).
// HTTP query parameters arrive as strings at the route layer; the route handler
// coerces "true"/"false" strings manually because the route uses escapeHatch
// for query validation rather than applying this Zod schema at runtime.
applyToAllCases: z.boolean().optional(),
});

export type FieldDefinitionsFindRequest = z.infer<typeof FieldDefinitionsFindRequestSchema>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ export const FieldDefinitionSchema = z.object({
* Optional human-readable description of the field's purpose
*/
description: z.string().optional(),

/**
* When true, this field is rendered in every case regardless of which template
* (if any) the case uses. Values are stored in extended_fields alongside
* template-specific fields.
*/
applyToAllCases: z.boolean().optional(),
});

export type FieldDefinition = z.infer<typeof FieldDefinitionSchema>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@ const validateField = (field: InlineField, value: string, errors: string[]): voi

export const validateExtendedFields = (
extendedFields: Record<string, string>,
fields: Array<RefField | InlineField>
fields: Array<RefField | InlineField>,
{ partial = false }: { partial?: boolean } = {}
): string[] => {
const errors: string[] = [];
const inlineFields = fields.filter(isInlineField);
Expand Down Expand Up @@ -146,8 +147,10 @@ export const validateExtendedFields = (
field.display?.show_when != null &&
!evaluateCondition(field.display.show_when, fieldValues, fieldTypeMap);

if (!isHidden) {
const value = fieldValues[field.name];
// In partial-update mode, skip fields not present in the request — the server
// merges them so an absent key retains its existing stored value.
const value = fieldValues[field.name];
if (!isHidden && !(partial && value === undefined)) {
const isArrayField =
field.control === FieldType.CHECKBOX_GROUP || field.control === FieldType.USER_PICKER;
const isEmpty =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@
* 2.0.
*/

import { getFieldCamelKey, getFieldSnakeKey } from './template_fields';
import yaml from 'js-yaml';
import {
getFieldCamelKey,
getFieldSnakeKey,
parseFieldDefinitionsToInlineFields,
} from './template_fields';
import type { FieldDefinition } from '../types/domain/field_definition/latest';

describe('template field key utils', () => {
describe('getFieldSnakeKey', () => {
Expand Down Expand Up @@ -44,4 +50,49 @@ describe('template field key utils', () => {
);
});
});

describe('parseFieldDefinitionsToInlineFields', () => {
const makeDef = (
overrides: Partial<FieldDefinition> & { defYaml?: object } = {}
): FieldDefinition => {
const { defYaml, ...rest } = overrides;
return {
fieldDefinitionId: 'fd-1',
name: 'my_field',
owner: 'securitySolution',
description: '',
applyToAllCases: true,
definition: yaml.dump(
defYaml ?? { name: 'my_field', type: 'keyword', control: 'INPUT_TEXT', label: 'My Field' }
),
...rest,
};
};

it('returns inline fields for valid definitions', () => {
const fields = parseFieldDefinitionsToInlineFields([makeDef()]);
expect(fields).toHaveLength(1);
expect(fields[0].name).toBe('my_field');
});

it('returns an empty array for an empty input', () => {
expect(parseFieldDefinitionsToInlineFields([])).toEqual([]);
});

it('skips definitions with malformed YAML', () => {
const bad = makeDef({ definition: 'not: valid: yaml: [broken' });
const good = makeDef({
name: 'ok',
definition: yaml.dump({ name: 'ok', type: 'keyword', control: 'INPUT_TEXT', label: 'OK' }),
});
const fields = parseFieldDefinitionsToInlineFields([bad, good]);
expect(fields).toHaveLength(1);
expect(fields[0].name).toBe('ok');
});

it('skips definitions that fail FieldSchema validation', () => {
const invalid = makeDef({ defYaml: { not_a_valid_field: true } });
expect(parseFieldDefinitionsToInlineFields([invalid])).toHaveLength(0);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,32 @@
*/

import { camelCase } from 'lodash';
import { load as parseYaml } from 'js-yaml';
import { FieldSchema, isInlineField } from '../types/domain/template/fields';
import type { InlineField } from '../types/domain/template/fields';
import type { FieldDefinition } from '../types/domain/field_definition/latest';

export const getFieldSnakeKey = (name: string, type: string): string => `${name}_as_${type}`;

export const getFieldCamelKey = (name: string, type: string): string =>
camelCase(getFieldSnakeKey(name, type));

/**
* Parses an array of field definitions into resolved inline fields, skipping any
* definitions that are malformed or describe reference (non-inline) fields.
*/
export const parseFieldDefinitionsToInlineFields = (defs: FieldDefinition[]): InlineField[] => {
const fields: InlineField[] = [];
for (const fd of defs) {
try {
const parsed = parseYaml(fd.definition);
const result = FieldSchema.safeParse(parsed);
if (result.success && isInlineField(result.data)) {
fields.push(result.data as InlineField);
}
} catch {
// Ignore malformed definitions
}
}
return fields;
};
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { isLegacyAttachmentRequest } from '../../../../common/utils/attachments'

jest.mock('./template_fields', () => ({
TemplateFields: () => <div data-test-subj="case-view-template-fields" />,
GlobalCaseFields: () => null,
}));

jest.mock('../../../containers/use_infinite_find_case_user_actions');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import { parseCaseUsers } from '../../utils';
import { CustomFields } from './custom_fields';
import { useReplaceCustomField } from '../../../containers/use_replace_custom_field';
import { KibanaServices } from '../../../common/lib/kibana';
import { TemplateFields } from './template_fields';
import { GlobalCaseFields, TemplateFields } from './template_fields';
import { useStatusAction } from '../../actions/status/use_status_action';
import { useRefreshCaseViewPage } from '../use_on_refresh_case_view_page';

Expand Down Expand Up @@ -353,7 +353,10 @@ export const CaseViewActivity = ({
onSubmit={onSubmitCustomField}
/>
{isTemplatesV2Enabled && (
<TemplateFields caseData={caseData} onUpdateField={onUpdateField} />
<div>
<TemplateFields caseData={caseData} onUpdateField={onUpdateField} />
<GlobalCaseFields caseData={caseData} onUpdateField={onUpdateField} />
</div>
)}
</EuiFlexGroup>
</EuiFlexItem>
Expand Down
Loading
Loading