diff --git a/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/generator/CamelYamlDslSchemaProcessorTest.java b/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/generator/CamelYamlDslSchemaProcessorTest.java index 2681264e4..70d8c6f7d 100644 --- a/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/generator/CamelYamlDslSchemaProcessorTest.java +++ b/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/generator/CamelYamlDslSchemaProcessorTest.java @@ -162,7 +162,7 @@ void testGetProcessors() throws Exception { var marshal = processorMap.get("org.apache.camel.model.MarshalDefinition"); assertFalse(marshal.has("anyOf")); - assertEquals("dataformat", marshal.get("$comment").asText()); + assertTrue(marshal.has("oneOf")); var toD = processorMap.get("org.apache.camel.model.ToDynamicDefinition"); assertTrue(toD.withObject("/properties").has("uri")); diff --git a/packages/ui/src/components/Form/OneOf/OneOfField.tsx b/packages/ui/src/components/Form/OneOf/OneOfField.tsx index d84b06c41..cfc19cc09 100644 --- a/packages/ui/src/components/Form/OneOf/OneOfField.tsx +++ b/packages/ui/src/components/Form/OneOf/OneOfField.tsx @@ -53,9 +53,6 @@ export const OneOfField = connectField(({ name: propsName, oneOf, onChange }: On 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); @@ -63,12 +60,24 @@ export const OneOfField = connectField(({ name: propsName, oneOf, onChange }: On return; } + /** Remove existing properties */ + const currentSchema = oneOfSchemas.find((schema) => schema.name === selectedSchemaName); + if (isDefined(currentSchema) && isDefined(appliedSchema)) { + const cleanModel = Object.keys(currentSchema.schema.properties ?? []).reduce((acc, key) => { + acc[key] = undefined; + return acc; + }, appliedSchema.model); + + const path = propsName === '' ? ROOT_PATH : `${propsName}.${selectedSchemaName}`; + onChange(cleanModel, path); + } + 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], + [appliedSchema, onChange, oneOfSchemas, propsName, schemaBridge?.schema, selectedSchemaName], ); useEffect(() => { @@ -78,6 +87,8 @@ export const OneOfField = connectField(({ name: propsName, oneOf, onChange }: On const handleOnChangeIndividualProp = useCallback( (path: string, value: unknown) => { const updatedPath = propsName === '' ? path : `${propsName}.${path}`; + console.log(path, updatedPath, value); + onChange(value, updatedPath); }, [onChange, propsName], diff --git a/packages/ui/src/components/Form/OneOf/OneOfSchemaList.tsx b/packages/ui/src/components/Form/OneOf/OneOfSchemaList.tsx index b137e7a7a..f6f54bb64 100644 --- a/packages/ui/src/components/Form/OneOf/OneOfSchemaList.tsx +++ b/packages/ui/src/components/Form/OneOf/OneOfSchemaList.tsx @@ -1,19 +1,10 @@ -import { - Dropdown, - DropdownItem, - DropdownList, - MenuToggle, - MenuToggleElement, - Text, - TextContent, - TextVariants, -} from '@patternfly/react-core'; +import { MenuToggle, MenuToggleElement, Text, TextContent, TextVariants } from '@patternfly/react-core'; import { FunctionComponent, PropsWithChildren, Ref, useCallback, useEffect, useMemo, useState } from 'react'; import { OneOfSchemas } from '../../../utils/get-oneof-schema-list'; import { isDefined } from '../../../utils/is-defined'; +import { Typeahead, TypeaheadOptions } from '../../Typeahead'; import { SchemaService } from '../schema.service'; import './OneOfSchemaList.scss'; -import { TypeaheadField } from '../customField/TypeaheadField'; interface OneOfComponentProps extends PropsWithChildren { name: string; @@ -32,10 +23,10 @@ export const OneOfSchemaList: FunctionComponent = ({ const [isOpen, setIsOpen] = useState(false); const onSelect = useCallback( - (_event: unknown, value: string | number | undefined) => { + (value: string) => { setIsOpen(false); onSchemaChanged(undefined); - onSchemaChanged(value as string); + onSchemaChanged(value); }, [onSchemaChanged], ); @@ -65,6 +56,16 @@ export const OneOfSchemaList: FunctionComponent = ({ [isOpen, name, onToggleClick, selectedSchemaName], ); + const options = useMemo(() => { + return oneOfSchemas.map((schemaDef): TypeaheadOptions => { + return { + value: schemaDef.name, + description: schemaDef.description, + isSelected: schemaDef.name === selectedSchemaName, + }; + }); + }, [oneOfSchemas, selectedSchemaName]); + useEffect(() => { if (oneOfSchemas.length === 1 && isDefined(oneOfSchemas[0]) && !selectedSchemaName) { onSchemaChanged(oneOfSchemas[0].name); @@ -77,7 +78,7 @@ export const OneOfSchemaList: FunctionComponent = ({ return ( <> - = ({ ); })} - + */} + + {children} diff --git a/packages/ui/src/components/Typeahead/Typeahead.tsx b/packages/ui/src/components/Typeahead/Typeahead.tsx new file mode 100644 index 000000000..9298ab1e6 --- /dev/null +++ b/packages/ui/src/components/Typeahead/Typeahead.tsx @@ -0,0 +1,250 @@ +import { + Button, + MenuToggle, + MenuToggleElement, + Select, + SelectList, + SelectOption, + SelectOptionProps, + TextInputGroup, + TextInputGroupMain, + TextInputGroupUtilities, +} from '@patternfly/react-core'; +import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon'; +import React, { FunctionComponent, useEffect, useMemo, useRef, useState } from 'react'; +import { IDataTestID } from '../../models'; + +export interface TypeaheadOptions extends IDataTestID { + value: string; + description?: string; + className?: string; + isDisabled?: boolean; + isSelected?: boolean; +} + +interface TypeaheadProps extends IDataTestID { + value?: string; + options: Array; + onChange?: (value: string) => void; + canCreateNewOption?: boolean; +} + +export const Typeahead: FunctionComponent = ({ + value, + options = [], + onChange, + canCreateNewOption = false, + 'data-testid': dataTestId = 'typeahead', +}) => { + const selectedReference = value ?? ''; + + const listOptions: TypeaheadOptions[] = useMemo(() => { + const localOptions: TypeaheadOptions[] = []; + const hasSelectedReference = options.some((option) => option.value === selectedReference); + + if (!hasSelectedReference && selectedReference !== '') { + const newOption: TypeaheadOptions = { + value: selectedReference, + isSelected: true, + }; + localOptions.push(newOption); + } + + return localOptions.concat(options); + }, [options, selectedReference]); + + const [isOpen, setIsOpen] = useState(false); + const [selected, setSelected] = useState(''); + const [inputValue, setInputValue] = useState(selectedReference); + const [filterValue, setFilterValue] = useState(''); + const [selectOptions, setSelectOptions] = useState(listOptions); + const [focusedItemIndex, setFocusedItemIndex] = useState(null); + const [activeItem, setActiveItem] = useState(null); + const [onCreation, setOnCreation] = useState(false); // Boolean to refresh filter state after new option is created + const textInputRef = useRef(); + + useEffect(() => { + let newSelectOptions: SelectOptionProps[] = [...listOptions]; + const lowerCaseFilterValue = filterValue.toLowerCase(); + + // Filter menu items based on the text input value when one exists + if (filterValue) { + newSelectOptions = listOptions.filter( + (menuItem) => + menuItem.value.toLocaleLowerCase().includes(lowerCaseFilterValue) || + menuItem.description?.toLocaleLowerCase().includes(lowerCaseFilterValue), + ); + + // When no options are found after filtering, display creation option + if (canCreateNewOption && !newSelectOptions.length) { + newSelectOptions = [{ isDisabled: false, children: `Insert custom value "${filterValue}"`, value: 'create' }]; + } + + // Open the menu when the input value changes and the new value is not empty + if (!isOpen) { + setIsOpen(true); + } + } + setSelectOptions(newSelectOptions); + setActiveItem(null); + setFocusedItemIndex(null); + }, [canCreateNewOption, filterValue, isOpen, listOptions]); + + const onToggleClick = () => { + setIsOpen(!isOpen); + }; + + const onSelect = (_event: unknown, value: string | number | undefined) => { + // if (value === 'create') { + // if (!listOptions.some((item) => item.value === filterValue)) { + // listOptions = [...listOptions, { value: filterValue }]; + // } + // setSelected(filterValue); + // setOnCreation(!onCreation); + // onChange?.(filterValue); + // setFilterValue(''); + // } else { + setInputValue(value as string); + setFilterValue(''); + setSelected(value as string); + onChange?.(value as string); + // } + setIsOpen(false); + setFocusedItemIndex(null); + setActiveItem(null); + }; + + const onTextInputChange = (_event: unknown, value: string) => { + setInputValue(value); + setFilterValue(value); + }; + const handleMenuArrowKeys = (key: string) => { + let indexToFocus; + + if (isOpen) { + if (key === 'ArrowUp') { + // When no index is set or at the first index, focus to the last, otherwise decrement focus index + if (focusedItemIndex === null || focusedItemIndex === 0) { + indexToFocus = selectOptions.length - 1; + } else { + indexToFocus = focusedItemIndex - 1; + } + } + + if (key === 'ArrowDown') { + // When no index is set or at the last index, focus to the first, otherwise increment focus index + if (focusedItemIndex === null || focusedItemIndex === selectOptions.length - 1) { + indexToFocus = 0; + } else { + indexToFocus = focusedItemIndex + 1; + } + } + + setFocusedItemIndex(indexToFocus ?? null); + const focusedItem = selectOptions.filter((option) => !option.isDisabled)[indexToFocus!]; + setActiveItem(`select-create-typeahead-${focusedItem.value.replace(' ', '-')}`); + } + }; + + const onInputKeyDown = (event: React.KeyboardEvent) => { + const enabledMenuItems = selectOptions.filter((option) => !option.isDisabled); + const [firstMenuItem] = enabledMenuItems; + const focusedItem = focusedItemIndex ? enabledMenuItems[focusedItemIndex] : firstMenuItem; + + switch (event.key) { + // Select the first available option + case 'Enter': + if (isOpen) { + onSelect(undefined, focusedItem.value as string); + setIsOpen((prevIsOpen) => !prevIsOpen); + setFocusedItemIndex(null); + setActiveItem(null); + } + + setIsOpen((prevIsOpen) => !prevIsOpen); + setFocusedItemIndex(null); + setActiveItem(null); + + break; + case 'Tab': + case 'Escape': + setIsOpen(false); + setActiveItem(null); + break; + case 'ArrowUp': + case 'ArrowDown': + event.preventDefault(); + handleMenuArrowKeys(event.key); + break; + } + }; + + const toggle = (toggleRef: React.Ref) => ( + + + + + + {!!inputValue && ( + + )} + + + + ); + + return ( + + ); +}; diff --git a/packages/ui/src/components/Typeahead/index.ts b/packages/ui/src/components/Typeahead/index.ts new file mode 100644 index 000000000..a47d88f96 --- /dev/null +++ b/packages/ui/src/components/Typeahead/index.ts @@ -0,0 +1 @@ +export * from './Typeahead'; diff --git a/packages/ui/src/hooks/applied-schema.hook.tsx b/packages/ui/src/hooks/applied-schema.hook.tsx index 344f22068..48098f30a 100644 --- a/packages/ui/src/hooks/applied-schema.hook.tsx +++ b/packages/ui/src/hooks/applied-schema.hook.tsx @@ -32,6 +32,7 @@ export const useAppliedSchema = (fieldName: string, oneOfSchemas: OneOfSchemas[] } const foundSchema = oneOfSchemas[index]; + console.log(JSON.stringify(form.model, null, 2)); return { index,