Skip to content

Commit

Permalink
feat(Form): Support anyOf & oneOf
Browse files Browse the repository at this point in the history
This commit adds support for anyOf & oneOf so it better reflects the
Caml YAML DSL schema.

fix: KaotoIO#1934
fix: KaotoIO#1913
fix: KaotoIO#794
  • Loading branch information
lordrip committed Jan 23, 2025
1 parent 102da40 commit d57b3f0
Show file tree
Hide file tree
Showing 32 changed files with 2,660 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { StepExpressionEditor } from '../../../Form/stepExpression/StepExpressio
import { UnknownNode } from '../../Custom/UnknownNode';
import { CanvasNode } from '../canvas.models';
import { CanvasFormTabsContext } from '../../../../providers/canvas-form-tabs.provider';
import { KaotoForm } from '../FormV2/KaotoForm';

interface CanvasFormTabsProps {
selectedNode: CanvasNode;
Expand Down Expand Up @@ -84,27 +85,28 @@ export const CanvasFormBody: FunctionComponent<CanvasFormTabsProps> = (props) =>
{stepFeatures.isUnknownComponent ? (
<UnknownNode model={model} />
) : (
<SchemaBridgeProvider schema={processedSchema} parentRef={divRef}>
{stepFeatures.isExpressionAwareStep && (
<StepExpressionEditor selectedNode={props.selectedNode} formMode={selectedTab} />
)}
{stepFeatures.isDataFormatAwareStep && (
<DataFormatEditor selectedNode={props.selectedNode} formMode={selectedTab} />
)}
{stepFeatures.isLoadBalanceAwareStep && (
<LoadBalancerEditor selectedNode={props.selectedNode} formMode={selectedTab} />
)}
<CustomAutoForm
key={props.selectedNode.id}
ref={formRef}
model={model}
onChange={handleOnChangeIndividualProp}
sortFields={false}
omitFields={omitFields.current}
data-testid="autoform"
/>
<div data-testid="root-form-placeholder" ref={divRef} />
</SchemaBridgeProvider>
// <SchemaBridgeProvider schema={processedSchema} parentRef={divRef}>
// {stepFeatures.isExpressionAwareStep && (
// <StepExpressionEditor selectedNode={props.selectedNode} formMode={selectedTab} />
// )}
// {stepFeatures.isDataFormatAwareStep && (
// <DataFormatEditor selectedNode={props.selectedNode} formMode={selectedTab} />
// )}
// {stepFeatures.isLoadBalanceAwareStep && (
// <LoadBalancerEditor selectedNode={props.selectedNode} formMode={selectedTab} />
// )}
// <CustomAutoForm
// key={props.selectedNode.id}
// ref={formRef}
// model={model}
// onChange={handleOnChangeIndividualProp}
// sortFields={false}
// omitFields={omitFields.current}
// data-testid="autoform"
// />
// <div data-testid="root-form-placeholder" ref={divRef} />
// </SchemaBridgeProvider>
<KaotoForm schema={processedSchema} onChange={handleOnChangeIndividualProp} model={model} />
)}
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Form } from '@patternfly/react-core';
import { FunctionComponent, useCallback } from 'react';
import { KaotoSchemaDefinition } from '../../../../models';
import { isDefined, ROOT_PATH } from '../../../../utils';
import { AutoField } from './fields/AutoField';
import { FormComponentFactoryProvider } from './providers/FormComponentFactoryProvider';
import { ModelContextProvider } from './providers/ModelProvider';
import { SchemaDefinitionsProvider } from './providers/SchemaDefinitionsProvider';
import { SchemaProvider } from './providers/SchemaProvider';

interface FormProps {
schema?: KaotoSchemaDefinition['schema'];
onChange: (propName: string, value: any) => void;
model: any;
}

export const KaotoForm: FunctionComponent<FormProps> = ({ schema, onChange, model }) => {
const onPropertyChange = useCallback(
(propName: string, value: any) => {
console.log('KaotoForm.onPropertyChange', propName, value);
onChange(propName, value);
},
[onChange],
);

if (!isDefined(schema)) {
return <div>Schema not defined</div>;
}

return (
<FormComponentFactoryProvider>
<SchemaDefinitionsProvider schema={schema}>
<SchemaProvider schema={schema}>
<ModelContextProvider model={model} onPropertyChange={onPropertyChange}>
<Form>
<AutoField propName={ROOT_PATH} />
</Form>
</ModelContextProvider>
</SchemaProvider>
</SchemaDefinitionsProvider>
</FormComponentFactoryProvider>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { FunctionComponent, useContext } from 'react';
import { KaotoSchemaDefinition } from '../../../../../models';
import { SchemaContext, SchemaProvider } from '../providers/SchemaProvider';
import { FieldProps } from '../typings';
import { AutoField } from './AutoField';

interface AnyOfFieldProps extends FieldProps {
anyOf: KaotoSchemaDefinition['schema']['anyOf'];
}

export const AnyOfField: FunctionComponent<AnyOfFieldProps> = ({ propName, anyOf }) => {
const { schema } = useContext(SchemaContext);

if (!Array.isArray(schema.anyOf) || schema.anyOf.length === 0) {
return null;
} else if (!schema) {
return <div>AnyOfField - Schema not defined</div>;
}

return (
<>
{anyOf?.map((schema, index) => {
return (
<SchemaProvider key={index} schema={schema}>
<AutoField propName={propName} />
</SchemaProvider>
);
})}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { FunctionComponent, useContext } from 'react';
import { FormComponentFactoryContext } from '../providers/FormComponentFactoryProvider';
import { SchemaContext } from '../providers/SchemaProvider';
import { FieldProps } from '../typings';

export const AutoField: FunctionComponent<FieldProps> = ({ propName, required }) => {
const { schema } = useContext(SchemaContext);
const formComponentFactory = useContext(FormComponentFactoryContext);

if (!schema) {
return <div>AutoField - Schema not defined</div>;
}
if (!formComponentFactory) {
return <div>AutoField - Form component factory not defined</div>;
}

const FieldComponent = formComponentFactory(schema);

return <FieldComponent propName={propName} required={required} />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Checkbox, FormGroup, FormGroupLabelHelp, Popover } from '@patternfly/react-core';
import { FunctionComponent, useContext } from 'react';
import { useFieldValue } from '../hooks/field-value';
import { SchemaContext } from '../providers/SchemaProvider';
import { FieldProps } from '../typings';

export const BooleanField: FunctionComponent<FieldProps> = ({ propName, required }) => {
const { schema } = useContext(SchemaContext);
const { value, onChange } = useFieldValue<boolean>(propName);
const onFieldChange = (_event: unknown, checked: boolean) => {
onChange(checked);
};

if (!schema) {
return <div>BooleanField - Schema not defined</div>;
}

const id = `${propName}-popover`;

return (
<FormGroup
fieldId={propName}
label={`${schema.title} (${propName})`}
isRequired={required}
labelHelp={
<Popover
id={id}
headerContent={<p>{schema.title}</p>}
bodyContent={<p>{schema.description}</p>}
footerContent={<p>Default: {schema.default?.toString() ?? 'no default value'}</p>}
triggerAction="hover"
withFocusTrap={false}
>
<FormGroupLabelHelp aria-label={`More info for ${schema.title} field`} />
</Popover>
}
>
<Checkbox
id={propName}
name={propName}
aria-describedby={id}
isChecked={value}
checked={value}
onChange={onFieldChange}
/>
</FormGroup>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { render } from '@testing-library/react';
import { DisabledField } from './DisabledField';

describe('DisabledField', () => {
it('should render', () => {
const { container } = render(<DisabledField data-testid="disabled-field-id" propName="test" />);

expect(container).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Card, CardBody, CardTitle } from '@patternfly/react-core';
import { FunctionComponent } from 'react';
import { IDataTestID } from '../../../../../models';
import { FieldProps } from '../typings';
import { CustomExpandableSection } from '../../../../Form/customField/CustomExpandableSection';

export const DisabledField: FunctionComponent<IDataTestID & FieldProps> = (props) => {
return (
<Card>
<CardTitle>{props.propName}</CardTitle>
<CardBody>
<p>Configuring this field is not yet supported</p>

<CustomExpandableSection groupName={props.propName}>
<code>
<pre>{JSON.stringify(props, null, 2)}</pre>
</code>
</CustomExpandableSection>
</CardBody>
</Card>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { FunctionComponent, useContext } from 'react';
import { ROOT_PATH } from '../../../../../../utils';
import { SchemaContext } from '../../providers/SchemaProvider';
import { FieldProps } from '../../typings';
import { ObjectFieldInner } from './ObjectFieldInner';
import { ObjectFieldWrapper } from './ObjectFieldWrapper';

export const ObjectField: FunctionComponent<FieldProps> = ({ propName }) => {
const { schema } = useContext(SchemaContext);
if (!schema) {
return <div>ObjectField - Schema not defined</div>;
}

if (propName === ROOT_PATH || !schema.title) {
return <ObjectFieldInner propName={propName} />;
}

return (
<ObjectFieldWrapper title={schema.title}>
<ObjectFieldInner propName={propName} />
</ObjectFieldWrapper>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { FunctionComponent, useContext } from 'react';
import { isDefined } from '../../../../../../utils';
import { SchemaContext, SchemaProvider } from '../../providers/SchemaProvider';
import { FieldProps } from '../../typings';
import { AnyOfField } from '../AnyOfField';
import { AutoField } from '../AutoField';

export const ObjectFieldInner: FunctionComponent<FieldProps> = ({ propName }) => {
const { schema } = useContext(SchemaContext);
if (!schema) {
return <div>ObjectField - Schema not defined</div>;
}

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

return (
<>
{Object.entries(schema.properties ?? {})
.filter(([_, propertySchema]) => {
/** Remove empty properties like `csimple: {}` */
return isDefined(propertySchema) && Object.keys(propertySchema).length > 0;
})
.map(([propertyName, propertySchema]) => {
const name = `${propName}.${propertyName}`;
const required = requiredProperties.includes(propertyName);

return (
<SchemaProvider key={name} schema={propertySchema}>
<AutoField propName={name} required={required} />
</SchemaProvider>
);
})}

{Array.isArray(schema.anyOf) && <AnyOfField propName={propName} anyOf={schema.anyOf} />}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Card, CardBody, CardTitle } from '@patternfly/react-core';
import { FunctionComponent, PropsWithChildren } from 'react';

interface ObjectFieldWrapperProps {
title: string;
}

export const ObjectFieldWrapper: FunctionComponent<PropsWithChildren<ObjectFieldWrapperProps>> = ({
title,
children,
}) => {
return (
<Card>
<CardTitle>{title}</CardTitle>

<CardBody className="pf-v6-c-form">{children}</CardBody>
</Card>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { FunctionComponent, useContext, useMemo, useState } from 'react';
import { getAppliedSchemaIndexV2 } from '../../../../../../utils/get-applied-schema-index';
import { getOneOfSchemaListV2, OneOfSchemas } from '../../../../../../utils/get-oneof-schema-list';
import { useFieldValue } from '../../hooks/field-value';
import { SchemaContext, SchemaProvider } from '../../providers/SchemaProvider';
import { FieldProps } from '../../typings';
import { AutoField } from '../AutoField';
import { SchemaList } from './SchemaList';

export const OneOfField: FunctionComponent<FieldProps> = ({ propName }) => {
const { schema, definitions } = useContext(SchemaContext);
const { value } = useFieldValue<unknown>(propName);

const oneOfSchemas: OneOfSchemas[] = useMemo(
() => getOneOfSchemaListV2(schema.oneOf ?? [], definitions),
[definitions, schema.oneOf],
);
const appliedSchemaIndex = getAppliedSchemaIndexV2(value, oneOfSchemas, definitions);
const presetSchema = appliedSchemaIndex === -1 ? undefined : oneOfSchemas[appliedSchemaIndex];
const [selectedOneOfSchema, setSelectedOneOfSchema] = useState<OneOfSchemas | undefined>(presetSchema);

return (
<SchemaList
propName={propName}
selectedSchema={selectedOneOfSchema}
schemas={oneOfSchemas}
onChange={setSelectedOneOfSchema}
>
{selectedOneOfSchema && (
<SchemaProvider schema={selectedOneOfSchema.schema}>
<AutoField propName={propName} />
</SchemaProvider>
)}
</SchemaList>
);
};
Loading

0 comments on commit d57b3f0

Please sign in to comment.