Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
39 changes: 38 additions & 1 deletion src/form/fields/ObjectField/ObjectFieldGrouping.test.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { act, fireEvent, render } from '@testing-library/react';
import { FunctionComponent, PropsWithChildren } from 'react';
import { KaotoSchemaDefinition } from '../../models';
import { FilteredFieldContext } from '../../providers/filtered-field.provider';
import { ROOT_PATH } from '../../utils';
import { SchemaProvider } from '../../providers/SchemaProvider';
import { FormWrapper } from '../../testing/FormWrapper';
import { ObjectFieldGrouping } from './ObjectFieldGrouping';
import { JSONSchema4 } from 'json-schema';

describe('ObjectFieldGrouping', () => {
const schema: JSONSchema4 = {
Expand Down Expand Up @@ -111,4 +111,41 @@ describe('ObjectFieldGrouping', () => {
<FormWrapper>{children}</FormWrapper>
</SchemaProvider>
);

it('should strip spaces from the filter before matching', () => {
const wrapper = render(
<FilteredFieldContext.Provider
value={{ filteredFieldText: 'cor rel', onFilterChange: jest.fn(), isGroupExpanded: false }}
>
<ObjectFieldGrouping propName={ROOT_PATH} />
</FilteredFieldContext.Provider>,
{ wrapper: formWrapper },
);

const inputFields = wrapper.queryAllByRole('textbox');
expect(inputFields).toHaveLength(1);
expect(inputFields[0]).toHaveAttribute('name', '#.correlationExpression');
});

it('should mark required properties with the required indicator', () => {
const requiredSchema: JSONSchema4 = {
type: 'object',
required: ['id'],
properties: {
id: { type: 'string', title: 'Id' },
description: { type: 'string', title: 'Description' },
},
};

const { getByTestId } = render(
<SchemaProvider schema={requiredSchema}>
<FormWrapper>
<ObjectFieldGrouping propName={ROOT_PATH} />
</FormWrapper>
</SchemaProvider>,
);

expect(getByTestId('#.id__field-wrapper').querySelector('.pf-v6-c-form__label-required')).toBeInTheDocument();
expect(getByTestId('#.description__field-wrapper').querySelector('.pf-v6-c-form__label-required')).toBeNull();
});
});
14 changes: 9 additions & 5 deletions src/form/fields/ObjectField/ObjectFieldGrouping.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FunctionComponent, useContext, useMemo } from 'react';
import { FilteredFieldContext } from '../../providers/filtered-field.provider';
import { getFieldGroups, getFilteredProperties } from '../../utils';
import { ModelContext } from '../../providers/ModelProvider';
import { getFieldGroups, getFilteredProperties, safeGetValue } from '../../utils';
import { SchemaContext, SchemaProvider } from '../../providers/SchemaProvider';
import { FieldProps } from '../../models/typings';
import { AnyOfField } from './AnyOfField';
Expand All @@ -12,12 +13,15 @@ const SPACE_REGEX = /\s/g;
export const ObjectFieldGrouping: FunctionComponent<FieldProps> = ({ propName }) => {
const { schema } = useContext(SchemaContext);
const { filteredFieldText } = useContext(FilteredFieldContext);
const { model } = useContext(ModelContext);

const groupedProperties = useMemo(() => {
const filteredProperties = useMemo(() => {
const cleanQueryTerm = filteredFieldText.replace(SPACE_REGEX, '').toLowerCase();
const filteredProperties = getFilteredProperties(schema.properties, cleanQueryTerm);
return getFieldGroups(filteredProperties);
}, [filteredFieldText, schema.properties]);
const modelSlice = safeGetValue(model, propName.replace('#.', '')) as Record<string, unknown> | undefined;
return getFilteredProperties(schema.properties, cleanQueryTerm, undefined, modelSlice);
}, [filteredFieldText, schema.properties, model, propName]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const groupedProperties = useMemo(() => getFieldGroups(filteredProperties), [filteredProperties]);

const requiredProperties = Array.isArray(schema.required) ? schema.required : [];

Expand Down
239 changes: 239 additions & 0 deletions src/form/utils/get-filtered-properties.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { JSONSchema4 } from 'json-schema';
import { restSchemaProperties } from '../stubs/rest-schema-properties';
import { getFilteredProperties } from './get-filtered-properties';

Expand All @@ -18,4 +19,242 @@ describe('getFilteredProperties()', () => {
]);
expect(filteredSchema).toMatchSnapshot();
});

it('should return the original properties reference when the filter is empty and no fields are omitted', () => {
const properties: JSONSchema4['properties'] = {
metadata: {
type: 'object',
properties: {
displayName: { type: 'string', title: 'Display Name' },
},
},
};

const result = getFilteredProperties(properties, '');
expect(result).toBe(properties);
});

it('should shallowly omit fields without recursively rebuilding nested schemas when the filter is empty', () => {
const properties: JSONSchema4['properties'] = {
metadata: {
type: 'object',
properties: {
displayName: { type: 'string', title: 'Display Name' },
},
},
description: { type: 'string', title: 'Description' },
};

const result = getFilteredProperties(properties, '', ['description']);
expect(result).toEqual({ metadata: properties.metadata });
expect(result?.metadata).toBe(properties.metadata);
});

it('should match a property by its schema title when the key does not match', () => {
const properties: JSONSchema4['properties'] = {
icon: {
type: 'string',
title: 'Kamelet Icon',
description: 'The icon',
},
name: {
type: 'string',
title: 'Name',
description: 'The name',
},
};

const result = getFilteredProperties(properties, 'kamelet');
expect(Object.keys(result!)).toEqual(['icon']);
});

it('should match a property by its current runtime value', () => {
const properties: JSONSchema4['properties'] = {
timerName: { type: 'string', title: 'Timer Name' },
period: { type: 'string', title: 'Period' },
};
const model = { timerName: 'mySpecialTimer', period: '1000' };

const result = getFilteredProperties(properties, 'myspecialtimer', undefined, model);
expect(Object.keys(result!)).toEqual(['timerName']);

const result2 = getFilteredProperties(properties, '1000', undefined, model);
expect(Object.keys(result2!)).toEqual(['period']);
});

it('should return an empty object when properties is undefined', () => {
const result = getFilteredProperties(undefined, 'anything');
expect(result).toEqual({});
});

it('should surface a nested object field when a child property matches', () => {
const properties: JSONSchema4['properties'] = {
metadata: {
type: 'object',
title: 'Metadata',
properties: {
displayName: { type: 'string', title: 'Display Name' },
version: { type: 'string', title: 'Version' },
},
},
unrelated: { type: 'string', title: 'Unrelated' },
};

const result = getFilteredProperties(properties, 'display');
expect(Object.keys(result!)).toEqual(['metadata']);
expect((result!['metadata'] as JSONSchema4).properties).toEqual({
displayName: { type: 'string', title: 'Display Name' },
});

const noMatch = getFilteredProperties(properties, 'zzz');
expect(Object.keys(noMatch!)).toHaveLength(0);
});

it('should surface an array field when a null item is in the model', () => {
const properties: JSONSchema4['properties'] = {
items: {
type: 'array',
title: 'Items',
items: {
type: 'object',
properties: {
label: { type: 'string', title: 'Label' },
},
},
},
};
const model = { items: [null, { label: 'hello' }] };

const result = getFilteredProperties(properties, 'hello', undefined, model as Record<string, unknown>);
expect(Object.keys(result!)).toContain('items');
});

it('should surface an array field whose item properties match by schema title', () => {
const properties: JSONSchema4['properties'] = {
kameletProperties: {
type: 'array',
title: 'Properties',
items: {
type: 'object',
properties: {
name: { type: 'string', title: 'Property name' },
},
},
},
};

const result = getFilteredProperties(properties, 'property');
expect(Object.keys(result!)).toContain('kameletProperties');
});

it('should not surface an array field when no item property matches', () => {
const properties: JSONSchema4['properties'] = {
kameletProperties: {
type: 'array',
title: 'Properties',
items: {
type: 'object',
properties: {
name: { type: 'string', title: 'Property name' },
},
},
},
};

const result = getFilteredProperties(properties, 'zzz');
expect(Object.keys(result!)).toHaveLength(0);
});

it('should surface an array field when an item sub-property value matches the filter', () => {
const properties: JSONSchema4['properties'] = {
kameletProperties: {
type: 'array',
title: 'Properties',
items: {
type: 'object',
properties: {
name: { type: 'string', title: 'Property name' },
value: { type: 'string', title: 'Value' },
},
},
},
};
const model = { kameletProperties: [{ name: 'Test', value: '' }] };

const result = getFilteredProperties(properties, 'test', undefined, model);
expect(Object.keys(result!)).toContain('kameletProperties');

const result2 = getFilteredProperties(properties, 'zzz', undefined, model);
expect(Object.keys(result2!)).toHaveLength(0);
});

it('should match a plain-string array field on its key name', () => {
const properties: JSONSchema4['properties'] = {
headers: {
type: 'array',
title: 'Headers',
items: { type: 'string' },
},
};

const result = getFilteredProperties(properties, 'header');
expect(Object.keys(result!)).toContain('headers');

const result2 = getFilteredProperties(properties, 'zzz');
expect(Object.keys(result2!)).toHaveLength(0);
});

it('should surface an object field when the container key matches and no child matches', () => {
const properties: JSONSchema4['properties'] = {
metadata: {
type: 'object',
title: 'Metadata',
properties: {
displayName: { type: 'string', title: 'Display Name' },
},
},
unrelated: { type: 'string', title: 'Unrelated' },
};

// match by key
const byKey = getFilteredProperties(properties, 'metadata');
expect(Object.keys(byKey!)).toEqual(['metadata']);
expect(byKey!['metadata']).toBe(properties!['metadata']);

// match by title
const byTitle = getFilteredProperties(properties, 'metadat');
expect(Object.keys(byTitle!)).toEqual(['metadata']);

// no match
const noMatch = getFilteredProperties(properties, 'zzz');
expect(Object.keys(noMatch!)).toHaveLength(0);
});

it('should surface an array field when the container key matches and no child matches', () => {
const properties: JSONSchema4['properties'] = {
kameletProperties: {
type: 'array',
title: 'Kamelet Properties',
items: {
type: 'object',
properties: {
name: { type: 'string', title: 'Name' },
},
},
},
};

// match by key
const byKey = getFilteredProperties(properties, 'kameletproperties');
expect(Object.keys(byKey!)).toEqual(['kameletProperties']);
expect(byKey!['kameletProperties']).toBe(properties!['kameletProperties']);

// match by title
const byTitle = getFilteredProperties(properties, 'kamelet prop');
expect(Object.keys(byTitle!)).toEqual(['kameletProperties']);

// no match
const noMatch = getFilteredProperties(properties, 'zzz');
expect(Object.keys(noMatch!)).toHaveLength(0);
});
});
Loading
Loading