-
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
7 changed files
with
361 additions
and
15 deletions.
There are no files selected for viewing
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
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]); |
109 changes: 109 additions & 0 deletions
109
packages/ui/src/components/Form/OneOf/OneOfSchemaList.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,109 @@ | ||
import { | ||
Chip, | ||
Dropdown, | ||
DropdownItem, | ||
DropdownList, | ||
MenuToggle, | ||
MenuToggleElement, | ||
Text, | ||
TextContent, | ||
TextVariants, | ||
} from '@patternfly/react-core'; | ||
import { FunctionComponent, PropsWithChildren, Ref, useCallback, useEffect, useState } from 'react'; | ||
import { OneOfSchemas } from '../../../utils/get-oneof-schema-list'; | ||
import { isDefined } from '../../../utils/is-defined'; | ||
import { SchemaService } from '../schema.service'; | ||
|
||
interface OneOfComponentProps extends PropsWithChildren { | ||
name: string; | ||
oneOfSchemas: OneOfSchemas[]; | ||
selectedSchemaName?: string; | ||
onSchemaChanged: (name: string | undefined) => void; | ||
} | ||
|
||
export const OneOfSchemaList: FunctionComponent<OneOfComponentProps> = ({ | ||
name, | ||
oneOfSchemas, | ||
selectedSchemaName, | ||
onSchemaChanged, | ||
children, | ||
}) => { | ||
const [isOpen, setIsOpen] = useState(false); | ||
|
||
const onSelect = useCallback( | ||
(_event: unknown, value: string | number | undefined) => { | ||
setIsOpen(false); | ||
onSchemaChanged(value as string); | ||
}, | ||
[onSchemaChanged], | ||
); | ||
|
||
const onToggleClick = useCallback(() => { | ||
setIsOpen(!isOpen); | ||
}, [isOpen]); | ||
|
||
const toggle = useCallback( | ||
(toggleRef: Ref<MenuToggleElement>) => ( | ||
<MenuToggle ref={toggleRef} onClick={onToggleClick} isFullWidth isExpanded={isOpen}> | ||
{selectedSchemaName || ( | ||
<TextContent> | ||
<Text component={TextVariants.small}>{SchemaService.DROPDOWN_PLACEHOLDER}</Text> | ||
</TextContent> | ||
)} | ||
</MenuToggle> | ||
), | ||
[isOpen, onToggleClick, selectedSchemaName], | ||
); | ||
|
||
const onClearChip = useCallback(() => { | ||
onSchemaChanged(undefined); | ||
}, [onSchemaChanged]); | ||
|
||
useEffect(() => { | ||
if (oneOfSchemas.length === 1 && isDefined(oneOfSchemas[0]) && !selectedSchemaName) { | ||
onSchemaChanged(oneOfSchemas[0].name); | ||
} | ||
}, [onSchemaChanged, oneOfSchemas, selectedSchemaName]); | ||
|
||
if (oneOfSchemas.length === 1) { | ||
return children; | ||
} | ||
|
||
return ( | ||
<> | ||
{isDefined(selectedSchemaName) ? ( | ||
<Chip key={`${selectedSchemaName}-chip`} onClick={onClearChip}> | ||
{selectedSchemaName} | ||
</Chip> | ||
) : ( | ||
<Dropdown | ||
id={`${name}-oneof-select`} | ||
data-testid={`${name}-oneof-select`} | ||
isOpen={isOpen} | ||
selected={selectedSchemaName} | ||
onSelect={onSelect} | ||
onOpenChange={setIsOpen} | ||
toggle={toggle} | ||
isScrollable | ||
> | ||
<DropdownList data-testid={`${name}-oneof-select-dropdownlist`}> | ||
{oneOfSchemas.map((schemaDef) => { | ||
return ( | ||
<DropdownItem | ||
data-testid={`${name}-oneof-select-dropdownlist-${schemaDef.name}`} | ||
key={schemaDef.name} | ||
value={schemaDef.name} | ||
description={schemaDef.description} | ||
> | ||
{schemaDef.name} | ||
</DropdownItem> | ||
); | ||
})} | ||
</DropdownList> | ||
</Dropdown> | ||
)} | ||
|
||
{children} | ||
</> | ||
); | ||
}; |
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
Oops, something went wrong.