-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(CanvasForm): Add support for ErrorHandler
Currently, there's partial support for `oneOf` in the `uniforms` library. This commit adds a new `OneOfField` component that shows a selector to pick which schema should the form generate. Also, if a model is already defined, the closest schema is selected automatically. fix: #560 relates: #948
- Loading branch information
Showing
14 changed files
with
897 additions
and
15 deletions.
There are no files selected for viewing
11 changes: 11 additions & 0 deletions
11
packages/ui-tests/cypress/fixtures/flows/errorHandlerCR.yaml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
- errorHandler: | ||
defaultErrorHandler: | ||
level: ERROR | ||
redeliveryPolicy: | ||
backOffMultiplier: "2.0" | ||
collisionAvoidanceFactor: "0.15" | ||
maximumRedeliveryDelay: "60000" | ||
redeliveryDelay: "1000" | ||
retriesExhaustedLogLevel: ERROR | ||
retryAttemptedLogInterval: "1" | ||
retryAttemptedLogLevel: DEBUG |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
115 changes: 115 additions & 0 deletions
115
packages/ui/src/components/Form/OneOf/OneOfField.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
import { AutoForm } from '@kaoto-next/uniforms-patternfly'; | ||
import { act, fireEvent, render } from '@testing-library/react'; | ||
import { FunctionComponent, PropsWithChildren, useEffect, useRef, useState } from 'react'; | ||
import { KaotoSchemaDefinition } from '../../../models/kaoto-schema'; | ||
import { SchemaBridgeContext } from '../../../providers/schema-bridge.provider'; | ||
import { SchemaService } from '../schema.service'; | ||
import { OneOfField } from './OneOfField'; | ||
|
||
describe('OneOfField', () => { | ||
const oneOfSchema: KaotoSchemaDefinition['schema'] = { | ||
oneOf: [ | ||
{ title: 'Name schema', type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }, | ||
{ title: 'Number schema', type: 'object', properties: { amount: { type: 'number' } }, required: ['amount'] }, | ||
{ title: 'Boolean schema', type: 'object', properties: { isValid: { type: 'boolean' } }, required: ['isValid'] }, | ||
], | ||
type: 'object', | ||
properties: { | ||
name: {}, | ||
amount: {}, | ||
isValid: {}, | ||
}, | ||
}; | ||
|
||
it('should render', () => { | ||
const wrapper = render(<OneOfField name="" oneOf={oneOfSchema.oneOf} />, { | ||
wrapper: (props) => ( | ||
<UniformsWrapper model={{}} schema={oneOfSchema}> | ||
{props.children} | ||
</UniformsWrapper> | ||
), | ||
}); | ||
|
||
expect(wrapper.asFragment()).toMatchSnapshot(); | ||
}); | ||
|
||
it('should render correctly', () => { | ||
const wrapper = render(<OneOfField name="" oneOf={oneOfSchema.oneOf} />, { | ||
wrapper: (props) => ( | ||
<UniformsWrapper model={{}} schema={oneOfSchema}> | ||
{props.children} | ||
</UniformsWrapper> | ||
), | ||
}); | ||
|
||
const dropDownToggle = wrapper.getByText(SchemaService.DROPDOWN_PLACEHOLDER); | ||
expect(dropDownToggle).toBeInTheDocument(); | ||
}); | ||
|
||
it('should render the appropriate schema when given a matching model', () => { | ||
const wrapper = render(<OneOfField name="" oneOf={oneOfSchema.oneOf} />, { | ||
wrapper: (props) => ( | ||
<UniformsWrapper model={{ name: 'John Doe' }} schema={oneOfSchema}> | ||
{props.children} | ||
</UniformsWrapper> | ||
), | ||
}); | ||
|
||
const nameField = wrapper.getByTestId('text-field'); | ||
expect(nameField).toBeInTheDocument(); | ||
expect(nameField).toHaveAttribute('label', 'Name'); | ||
expect(nameField).toHaveValue('John Doe'); | ||
}); | ||
|
||
it('should render a new selected schema', async () => { | ||
const wrapper = render(<OneOfField name="" oneOf={oneOfSchema.oneOf} />, { | ||
wrapper: (props) => ( | ||
<UniformsWrapper model={{}} schema={oneOfSchema}> | ||
{props.children} | ||
</UniformsWrapper> | ||
), | ||
}); | ||
|
||
await act(async () => { | ||
const dropDownToggle = wrapper.getByText(SchemaService.DROPDOWN_PLACEHOLDER); | ||
expect(dropDownToggle).toBeInTheDocument(); | ||
|
||
fireEvent.click(dropDownToggle); | ||
}); | ||
|
||
await act(async () => { | ||
const schema2Item = wrapper.getByText('Number schema'); | ||
fireEvent.click(schema2Item); | ||
}); | ||
|
||
const nameField = wrapper.getByTestId('text-field'); | ||
expect(nameField).toBeInTheDocument(); | ||
expect(nameField).toHaveAttribute('label', 'Amount'); | ||
expect(nameField).toHaveValue(''); | ||
}); | ||
}); | ||
|
||
const UniformsWrapper: FunctionComponent< | ||
PropsWithChildren<{ | ||
model: Record<string, unknown>; | ||
schema: KaotoSchemaDefinition['schema']; | ||
}> | ||
> = (props) => { | ||
const schemaBridge = new SchemaService().getSchemaBridge(props.schema); | ||
const divRef = useRef<HTMLDivElement>(null); | ||
const [, setLastRenderTimestamp] = useState<number>(-1); | ||
|
||
useEffect(() => { | ||
/** Force re-render to update the divRef */ | ||
setLastRenderTimestamp(Date.now()); | ||
}, []); | ||
|
||
return ( | ||
<SchemaBridgeContext.Provider value={{ schemaBridge, parentRef: divRef }}> | ||
<AutoForm model={props.model} schema={schemaBridge}> | ||
{props.children} | ||
</AutoForm> | ||
<div ref={divRef} /> | ||
</SchemaBridgeContext.Provider> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; | ||
import { createPortal } from 'react-dom'; | ||
import { GuaranteedProps, connectField } from 'uniforms'; | ||
import { useAppliedSchema, useSchemaBridgeContext } from '../../../hooks'; | ||
import { KaotoSchemaDefinition } from '../../../models'; | ||
import { SchemaBridgeProvider } from '../../../providers/schema-bridge.provider'; | ||
import { ROOT_PATH, isDefined } from '../../../utils'; | ||
import { OneOfSchemas, getOneOfSchemaList } from '../../../utils/get-oneof-schema-list'; | ||
import { CustomAutoForm, CustomAutoFormRef } from '../CustomAutoForm'; | ||
import { SchemaService } from '../schema.service'; | ||
import { OneOfSchemaList } from './OneOfSchemaList'; | ||
|
||
interface OneOfComponentProps extends GuaranteedProps<unknown> { | ||
oneOf: KaotoSchemaDefinition['schema'][]; | ||
} | ||
|
||
const applyDefinitionsToSchema = ( | ||
schema?: KaotoSchemaDefinition['schema'], | ||
rootSchema?: KaotoSchemaDefinition['schema'], | ||
) => { | ||
if (!isDefined(schema) || !isDefined(rootSchema)) { | ||
return schema; | ||
} | ||
|
||
return Object.assign({}, schema, { | ||
definitions: rootSchema.definitions, | ||
}); | ||
}; | ||
|
||
const OneOfComponent: FunctionComponent<OneOfComponentProps> = ({ name: propsName, oneOf, onChange }) => { | ||
const formRef = useRef<CustomAutoFormRef>(null); | ||
const divRef = useRef<HTMLDivElement>(null); | ||
|
||
const { schemaBridge, parentRef } = useSchemaBridgeContext(); | ||
const oneOfSchemas: OneOfSchemas[] = useMemo( | ||
() => getOneOfSchemaList(oneOf, schemaBridge?.schema), | ||
[oneOf, schemaBridge?.schema], | ||
); | ||
|
||
const appliedSchema = useAppliedSchema(propsName, oneOfSchemas); | ||
const [selectedSchema, setSelectedSchema] = useState<KaotoSchemaDefinition['schema'] | undefined>( | ||
applyDefinitionsToSchema(appliedSchema?.schema, schemaBridge?.schema), | ||
); | ||
const [selectedSchemaName, setSelectedSchemaName] = useState<string | undefined>(appliedSchema?.name); | ||
|
||
const onSchemaChanged = useCallback( | ||
(schemaName: string | undefined) => { | ||
if (schemaName === selectedSchemaName) return; | ||
/** Remove existing properties */ | ||
const path = propsName === '' ? ROOT_PATH : `${propsName}.${selectedSchemaName}`; | ||
onChange(undefined, path); | ||
|
||
if (!isDefined(schemaName)) { | ||
setSelectedSchema(undefined); | ||
setSelectedSchemaName(undefined); | ||
return; | ||
} | ||
|
||
const selectedSchema = oneOfSchemas.find((schema) => schema.name === schemaName); | ||
const schemaWithDefinitions = applyDefinitionsToSchema(selectedSchema?.schema, schemaBridge?.schema); | ||
setSelectedSchema(schemaWithDefinitions); | ||
setSelectedSchemaName(schemaName); | ||
}, | ||
[onChange, oneOfSchemas, propsName, schemaBridge?.schema, selectedSchemaName], | ||
); | ||
|
||
useEffect(() => { | ||
formRef.current?.form.reset(); | ||
}, []); | ||
|
||
const handleOnChangeIndividualProp = useCallback( | ||
(path: string, value: unknown) => { | ||
const updatedPath = propsName === '' ? path : `${propsName}.${path}`; | ||
onChange(value, updatedPath); | ||
}, | ||
[onChange, propsName], | ||
); | ||
|
||
return ( | ||
<OneOfSchemaList | ||
name={propsName} | ||
oneOfSchemas={oneOfSchemas} | ||
selectedSchemaName={selectedSchemaName} | ||
onSchemaChanged={onSchemaChanged} | ||
> | ||
{isDefined(selectedSchema) && ( | ||
<SchemaBridgeProvider schema={selectedSchema} parentRef={divRef}> | ||
{isDefined(parentRef?.current) && | ||
createPortal( | ||
<> | ||
<CustomAutoForm | ||
key={propsName} | ||
ref={formRef} | ||
model={appliedSchema?.model ?? {}} | ||
onChange={handleOnChangeIndividualProp} | ||
sortFields={false} | ||
omitFields={SchemaService.OMIT_FORM_FIELDS} | ||
data-testid={`${propsName}-autoform`} | ||
/> | ||
<div data-testid={`${propsName}-form-placeholder`} ref={divRef} /> | ||
</>, | ||
parentRef.current, | ||
propsName, | ||
)} | ||
</SchemaBridgeProvider> | ||
)} | ||
</OneOfSchemaList> | ||
); | ||
}; | ||
|
||
export const OneOfField = connectField(OneOfComponent as unknown as Parameters<typeof connectField>[0]); |
92 changes: 92 additions & 0 deletions
92
packages/ui/src/components/Form/OneOf/OneOfSchemaList.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import { act, fireEvent, render } from '@testing-library/react'; | ||
import { OneOfSchemas } from '../../../utils/get-oneof-schema-list'; | ||
import { SchemaService } from '../schema.service'; | ||
import { OneOfSchemaList } from './OneOfSchemaList'; | ||
|
||
describe('OneOfSchemaList', () => { | ||
const oneOfSchemas: OneOfSchemas[] = [ | ||
{ name: 'Schema 1', schema: { type: 'object', properties: { name: { type: 'string' } } } }, | ||
{ name: 'Schema 2', schema: { type: 'object', properties: { amount: { type: 'number' } } } }, | ||
{ name: 'Schema 3', schema: { type: 'object', properties: { isValid: { type: 'boolean' } } } }, | ||
]; | ||
|
||
it('should render', () => { | ||
const wrapper = render(<OneOfSchemaList name="" oneOfSchemas={oneOfSchemas} onSchemaChanged={() => {}} />); | ||
|
||
expect(wrapper.asFragment()).toMatchSnapshot(); | ||
}); | ||
|
||
it('should render the dropwdown toggle correctly', () => { | ||
const wrapper = render(<OneOfSchemaList name="" oneOfSchemas={oneOfSchemas} onSchemaChanged={() => {}} />); | ||
|
||
const dropDownToggle = wrapper.getByText(SchemaService.DROPDOWN_PLACEHOLDER); | ||
expect(dropDownToggle).toBeInTheDocument(); | ||
}); | ||
|
||
it('should render a control with the selected schema name and provided children', () => { | ||
const wrapper = render( | ||
<OneOfSchemaList name="" oneOfSchemas={oneOfSchemas} selectedSchemaName="Schema 1" onSchemaChanged={() => {}}> | ||
<div data-testid="children">Children</div> | ||
</OneOfSchemaList>, | ||
); | ||
|
||
const control = wrapper.getByTestId('Schema 1-chip'); | ||
expect(control).toBeInTheDocument(); | ||
|
||
const children = wrapper.getByTestId('children'); | ||
expect(children).toBeInTheDocument(); | ||
}); | ||
|
||
it('should notify the parent when a schema is selected', async () => { | ||
const schemaChangedSpy = jest.fn(); | ||
|
||
const wrapper = render( | ||
<OneOfSchemaList | ||
name="" | ||
oneOfSchemas={oneOfSchemas} | ||
selectedSchemaName={undefined} | ||
onSchemaChanged={schemaChangedSpy} | ||
> | ||
<div data-testid="children">Children</div> | ||
</OneOfSchemaList>, | ||
); | ||
|
||
await act(async () => { | ||
const dropDownToggle = wrapper.getByText(SchemaService.DROPDOWN_PLACEHOLDER); | ||
expect(dropDownToggle).toBeInTheDocument(); | ||
|
||
fireEvent.click(dropDownToggle); | ||
}); | ||
|
||
await act(async () => { | ||
const schema2Item = wrapper.getByText('Schema 2'); | ||
fireEvent.click(schema2Item); | ||
}); | ||
|
||
expect(schemaChangedSpy).toHaveBeenCalledWith('Schema 2'); | ||
}); | ||
|
||
it('should notify the parent when a schema is deselected', async () => { | ||
const schemaChangedSpy = jest.fn(); | ||
|
||
const wrapper = render( | ||
<OneOfSchemaList | ||
name="" | ||
oneOfSchemas={oneOfSchemas} | ||
selectedSchemaName="Schema 2" | ||
onSchemaChanged={schemaChangedSpy} | ||
> | ||
<div data-testid="children">Children</div> | ||
</OneOfSchemaList>, | ||
); | ||
|
||
await act(async () => { | ||
const chip = wrapper.getByLabelText('close'); | ||
expect(chip).toBeInTheDocument(); | ||
|
||
fireEvent.click(chip); | ||
}); | ||
|
||
expect(schemaChangedSpy).toHaveBeenCalledWith(undefined); | ||
}); | ||
}); |
Oops, something went wrong.