From c8738a3206eaa7de4953a8972091c102812a720b Mon Sep 17 00:00:00 2001 From: Martin Marosi Date: Wed, 4 Feb 2026 15:47:05 +0100 Subject: [PATCH 1/4] fix(common): select and dual list select types --- .../src/dual-list-select/dual-list-select.tsx | 7 ++++++- packages/common/src/select/flat-options.ts | 20 ++++++------------- packages/common/src/select/select.tsx | 9 +++++++-- packages/common/src/use-select/reducer.ts | 14 ++++++------- packages/common/src/use-select/use-select.ts | 10 +++++----- 5 files changed, 31 insertions(+), 29 deletions(-) diff --git a/packages/common/src/dual-list-select/dual-list-select.tsx b/packages/common/src/dual-list-select/dual-list-select.tsx index bf6fa0a5a..ac644f2a9 100644 --- a/packages/common/src/dual-list-select/dual-list-select.tsx +++ b/packages/common/src/dual-list-select/dual-list-select.tsx @@ -15,7 +15,7 @@ interface DualListSelectComponentProps { handleMoveLeft: () => void; handleClearLeftValues: () => void; handleClearRightValues: () => void; - filterOptions?: (options: SelectOption[], filterValue: string) => SelectOption[]; + filterOptions?: (options: SelectOption[], filterValue: string) => SelectOption[] | ((value: string) => void); filterValue?: string; filterValueText?: string; filterPlaceholder?: string; @@ -23,6 +23,11 @@ interface DualListSelectComponentProps { filterValuePlaceholder?: string; noOptionsText?: string; noValueText?: string; + // Missing props that are actually used in implementation + sortOptions: () => void; + sortValues: () => void; + filterValues: (value: string) => void; + state: DualListSelectState; [key: string]: unknown; } diff --git a/packages/common/src/select/flat-options.ts b/packages/common/src/select/flat-options.ts index 0cb7855bf..701f6dccb 100644 --- a/packages/common/src/select/flat-options.ts +++ b/packages/common/src/select/flat-options.ts @@ -1,22 +1,14 @@ import { ReactNode } from "react"; +import { FlatSelectOption, OptionValue } from '../types/shared-types'; -interface Option { +export interface Option { label: string | ReactNode; value: any; selectAll?: boolean; selectNone?: boolean; } -interface ResultedOption { - label?: string | ReactNode; - value?: any; - selectAll?: boolean; - selectNone?: boolean; - group?: string | ReactNode; - divider?: boolean; -} - -interface Options { +export interface Options { label?: string | ReactNode; value?: any; divider?: boolean; @@ -25,8 +17,8 @@ interface Options { options?: Option[]; } -const flatOptions = (options: Options[]): ResultedOption[] => - options.flatMap((option) => (option.options ? [{ group: option.label }, ...option.options.map(opt => opt as ResultedOption)] : [option as ResultedOption])); +const flatOptions = (options: Options[]): FlatSelectOption[] => + options.flatMap((option) => (option.options ? [{ group: option.label } as FlatSelectOption, ...option.options.map(opt => opt as FlatSelectOption)] : [option as FlatSelectOption])); export default flatOptions; -export type { Option, ResultedOption, Options }; +export type ResultedOption = FlatSelectOption; diff --git a/packages/common/src/select/select.tsx b/packages/common/src/select/select.tsx index 8769d0ee9..52850767c 100644 --- a/packages/common/src/select/select.tsx +++ b/packages/common/src/select/select.tsx @@ -5,7 +5,7 @@ import { AnyObject } from "@data-driven-forms/react-form-renderer"; import clsx from 'clsx'; import useSelect from '../use-select/use-select'; import deepEqual from './deep-equal'; -import { SelectOption, OptionValue, SelectValue } from '../types/shared-types'; +import { SelectOption, OptionValue, SelectValue, FlatSelectOption } from '../types/shared-types'; export interface SelectProps { options?: SelectOption[]; @@ -26,12 +26,17 @@ export interface SelectProps { selectVariant?: string; updatingMessage?: React.ReactNode; noOptionsMessage?: React.ReactNode; + noResultsMessage?: React.ReactNode; isSearchable?: boolean; isClearable?: boolean; SelectComponent?: React.ComponentType; noValueUpdates?: boolean; - optionsTransformer?: (options: AnyObject[]) => SelectOption[]; + optionsTransformer?: (options: AnyObject[]) => FlatSelectOption[]; compareValues?: (valueA: T, valueB: T) => boolean; + menuIsPortal?: boolean; + menuPortalTarget?: Element; + showMoreLabel?: string; + showLessLabel?: string; } const Select = ({ diff --git a/packages/common/src/use-select/reducer.ts b/packages/common/src/use-select/reducer.ts index 9e1d6d23a..ee242dd25 100644 --- a/packages/common/src/use-select/reducer.ts +++ b/packages/common/src/use-select/reducer.ts @@ -1,29 +1,29 @@ import { AnyObject } from '@data-driven-forms/react-form-renderer'; -import { SelectOption, OptionValue } from '../types/shared-types'; +import { SelectOption, OptionValue, FlatSelectOption } from '../types/shared-types'; export interface SelectState { isLoading: boolean; - options: SelectOption[]; + options: (SelectOption | FlatSelectOption)[]; promises: AnyObject; isInitialLoaded: boolean; - originalOptions?: SelectOption[]; + originalOptions?: (SelectOption | FlatSelectOption)[]; } interface InitProps { propsOptions: SelectOption[]; - optionsTransformer?: (options: AnyObject[]) => SelectOption[]; + optionsTransformer?: (options: AnyObject[]) => FlatSelectOption[]; } type ReducerAction = - | { type: 'updateOptions'; payload: SelectOption[]; optionsTransformer?: (options: AnyObject[]) => SelectOption[] } + | { type: 'updateOptions'; payload: SelectOption[]; optionsTransformer?: (options: AnyObject[]) => FlatSelectOption[] } | { type: 'startLoading' } - | { type: 'setOptions'; payload: SelectOption[]; optionsTransformer?: (options: AnyObject[]) => SelectOption[] } + | { type: 'setOptions'; payload: SelectOption[]; optionsTransformer?: (options: AnyObject[]) => FlatSelectOption[] } | { type: 'initialLoaded' } | { type: 'setPromises'; payload: AnyObject; options?: SelectOption[]; - optionsTransformer?: (options: AnyObject[]) => SelectOption[]; + optionsTransformer?: (options: AnyObject[]) => FlatSelectOption[]; compareValues?: (value1: T, value2: T) => boolean; }; diff --git a/packages/common/src/use-select/use-select.ts b/packages/common/src/use-select/use-select.ts index 8db6fd628..838e2fef1 100644 --- a/packages/common/src/use-select/use-select.ts +++ b/packages/common/src/use-select/use-select.ts @@ -6,11 +6,11 @@ import { AnyObject } from '@data-driven-forms/react-form-renderer'; import useIsMounted from '../hooks/use-is-mounted'; import reducer, { init, SelectState } from './reducer'; import fnToString from '../utils/fn-to-string'; -import { SelectOption, OptionValue, SelectValue } from '../types/shared-types'; +import { SelectOption, OptionValue, SelectValue, FlatSelectOption } from '../types/shared-types'; interface UseSelectProps { loadOptions?: (inputValue?: string) => Promise[]>; - optionsTransformer?: (options: AnyObject[]) => SelectOption[]; + optionsTransformer?: (options: AnyObject[]) => FlatSelectOption[]; options?: SelectOption[]; noValueUpdates?: boolean; onChange?: (value?: SelectValue) => void; @@ -27,8 +27,8 @@ const getSelectValue = ( stateValue: SelectValue, simpleValue: boolean, isMulti: boolean, - allOptions: SelectOption[] -): SelectOption[] | SelectValue => { + allOptions: (SelectOption | FlatSelectOption)[] +): (SelectOption | FlatSelectOption)[] | SelectValue => { let enhancedValue = stateValue; let hasSelectAll = isMulti && allOptions.find(({ selectAll }) => selectAll); @@ -62,7 +62,7 @@ const handleSelectChange = ( simpleValue: boolean, isMulti: boolean, onChange: (value?: SelectValue) => void, - allOptions: SelectOption[], + allOptions: (SelectOption | FlatSelectOption)[], removeSelectAll: boolean, removeSelectNone: boolean ): void => { From 8b4d6b414b2a6421b2ec91d076aea7c0e8ddf378 Mon Sep 17 00:00:00 2001 From: Martin Marosi Date: Wed, 4 Feb 2026 15:47:58 +0100 Subject: [PATCH 2/4] fix(renderer): use field API and wizard context types --- .../react-form-renderer/src/use-field-api/use-field-api.ts | 2 +- .../src/wizard-context/wizard-context.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react-form-renderer/src/use-field-api/use-field-api.ts b/packages/react-form-renderer/src/use-field-api/use-field-api.ts index c06c4607d..5fcb31390 100644 --- a/packages/react-form-renderer/src/use-field-api/use-field-api.ts +++ b/packages/react-form-renderer/src/use-field-api/use-field-api.ts @@ -25,7 +25,7 @@ export interface UseFieldApiConfig extends AnyObject { useWarnings?: boolean; resolveProps?: (props: any, fieldProps: any, formOptions: any) => any; initializeOnMount?: boolean; - component: string; + component?: string; render?: any; clearOnUnmount?: boolean; dataType?: DataType; diff --git a/packages/react-form-renderer/src/wizard-context/wizard-context.ts b/packages/react-form-renderer/src/wizard-context/wizard-context.ts index 4e0bdbc96..08f90d7b3 100644 --- a/packages/react-form-renderer/src/wizard-context/wizard-context.ts +++ b/packages/react-form-renderer/src/wizard-context/wizard-context.ts @@ -17,13 +17,13 @@ export type NextStep = export interface WizardContextValue { formOptions: FormOptions; crossroads: string[]; - currentStep: { fields?: Field[]; name: string; title?: string; nextStep?: NextStep } | undefined; + currentStep: { fields?: Field[]; name: string; title?: string; nextStep?: NextStep; isProgressAfterSubmissionStep?: boolean } | undefined; handlePrev: Function; onKeyDown: Function; jumpToStep: Function; - setPrevSteps: Function; + setPrevSteps: () => void; handleNext: Function; - navSchema: Object; + navSchema: Record[]; activeStepIndex: number; maxStepIndex: number; isDynamic: boolean; From 599395f3005a98bda67628ebb29995cfdc39f612 Mon Sep 17 00:00:00 2001 From: Martin Marosi Date: Wed, 4 Feb 2026 15:49:27 +0100 Subject: [PATCH 3/4] chore(renderer): add missing demo tsconfig --- packages/react-form-renderer/.gitignore | 1 + .../react-form-renderer/tsconfig.demo.json | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 packages/react-form-renderer/tsconfig.demo.json diff --git a/packages/react-form-renderer/.gitignore b/packages/react-form-renderer/.gitignore index 2f08b8e28..da7d2908d 100644 --- a/packages/react-form-renderer/.gitignore +++ b/packages/react-form-renderer/.gitignore @@ -91,6 +91,7 @@ vendor !jest.config.ts !tsconfig.cjs.json !tsconfig.esm.json +!tsconfig.demo.json !generate-typings.js !rollup.config.js !generate-componen-examples.js diff --git a/packages/react-form-renderer/tsconfig.demo.json b/packages/react-form-renderer/tsconfig.demo.json new file mode 100644 index 000000000..f37217b80 --- /dev/null +++ b/packages/react-form-renderer/tsconfig.demo.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "sourceMap": true, + "module": "esnext", + "moduleResolution": "node", + "target": "es5", + "lib": ["es6", "dom"], + "jsx": "react-jsx", + "allowSyntheticDefaultImports": true, + "noErrorTruncation": true, + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "strict": false, + "noEmit": false, + "isolatedModules": false, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "noImplicitAny": false, + "noImplicitReturns": false, + "noImplicitThis": false, + "rootDir": "." + }, + "include": [ + "./demo/**/*", + "./src/**/*" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file From e22ef36b0163865eb8450080d7479a415d183b87 Mon Sep 17 00:00:00 2001 From: Martin Marosi Date: Wed, 4 Feb 2026 15:50:11 +0100 Subject: [PATCH 4/4] feat(pf): migrate PF mapper to TS --- package.json | 2 +- packages/pf4-component-mapper/.gitignore | 4 + .../config/rspack.config.js | 90 +++++ .../config/webpack.config.js | 1 - ...al-list-schema.js => dual-list-schema.tsx} | 0 .../demo-schemas/{sandbox.js => sandbox.ts} | 244 ++++++------ .../{select-schema.js => select-schema.tsx} | 25 +- .../{widget-schema.js => widget-schema.ts} | 74 +++- .../{wizard-schema.js => wizard-schema.tsx} | 336 ++++++++++------- .../demo/{index.js => index.tsx} | 59 ++- packages/pf4-component-mapper/jest.config.ts | 6 + packages/pf4-component-mapper/package.json | 9 +- .../src/checkbox/checkbox.d.ts | 20 - .../checkbox/{checkbox.js => checkbox.tsx} | 16 +- .../src/checkbox/index.js | 2 - .../src/checkbox/{index.d.ts => index.ts} | 0 .../src/checkbox/multiple-choice-list.js | 20 - .../src/checkbox/multiple-choice-list.tsx | 56 +++ .../component-mapper/component-mapper.d.ts | 47 --- ...omponent-mapper.js => component-mapper.ts} | 21 +- .../src/component-mapper/index.js | 2 - .../component-mapper/{index.d.ts => index.ts} | 0 .../src/date-picker/date-picker.d.ts | 9 - .../{date-picker.js => date-picker.tsx} | 6 +- .../src/date-picker/index.js | 2 - .../src/date-picker/{index.d.ts => index.tsx} | 1 - .../dual-list-context/dual-list-context.d.ts | 17 - ...-list-context.js => dual-list-context.tsx} | 2 +- .../src/dual-list-context/index.js | 2 - .../{index.d.ts => index.tsx} | 1 - .../dual-list-select/dual-list-select.d.ts | 22 -- .../src/dual-list-select/dual-list-select.js | 121 ------ .../src/dual-list-select/dual-list-select.tsx | 119 ++++++ .../src/dual-list-select/index.js | 2 - .../{index.d.ts => index.tsx} | 1 - .../dual-list-sort-button.d.ts | 9 - ...rt-button.js => dual-list-sort-button.tsx} | 7 +- .../src/dual-list-sort-button/index.js | 2 - .../{index.d.ts => index.tsx} | 1 - .../dual-list-tree-select.d.ts | 19 - ...ee-select.js => dual-list-tree-select.tsx} | 48 ++- .../src/dual-list-tree-select/index.d.ts | 2 - .../src/dual-list-tree-select/index.js | 2 - .../src/dual-list-tree-select/index.tsx | 1 + .../src/field-array/field-array.d.ts | 24 -- .../{field-array.js => field-array.tsx} | 50 +-- .../src/field-array/index.js | 2 - .../src/field-array/{index.d.ts => index.tsx} | 0 .../src/form-group/form-group.d.ts | 14 - .../{form-group.js => form-group.tsx} | 23 +- .../src/form-group/index.js | 2 - .../src/form-group/{index.d.ts => index.ts} | 0 .../src/form-template/form-template.d.ts | 5 - .../{form-template.js => form-template.tsx} | 21 +- .../src/form-template/index.d.ts | 2 - .../src/form-template/index.js | 2 - .../src/form-template/index.tsx | 1 + .../src/{index.js => index.ts} | 6 + .../src/is-required/index.js | 2 - .../src/is-required/{index.d.ts => index.ts} | 0 .../src/is-required/is-required.d.ts | 5 - .../{is-required.js => is-required.tsx} | 6 +- .../src/plain-text/index.js | 2 - .../src/plain-text/{index.d.ts => index.ts} | 0 .../src/plain-text/plain-text.d.ts | 15 - .../src/plain-text/plain-text.js | 17 - .../src/plain-text/plain-text.tsx | 27 ++ .../pf4-component-mapper/src/radio/index.js | 2 - .../src/radio/{index.d.ts => index.tsx} | 1 - .../pf4-component-mapper/src/radio/radio.d.ts | 21 -- .../src/radio/{radio.js => radio.tsx} | 17 +- .../pf4-component-mapper/src/select/index.js | 2 - .../src/select/{index.d.ts => index.ts} | 0 .../src/select/select.d.ts | 45 --- .../src/select/{select.js => select.tsx} | 25 +- ...clear-indicator.js => clear-indicator.tsx} | 7 +- .../{empty-options.js => empty-options.tsx} | 0 .../src/select/select/{input.js => input.tsx} | 20 +- .../src/select/select/{menu.js => menu.tsx} | 109 ++++-- .../select/select/{option.js => option.tsx} | 0 .../select/select/{select.js => select.tsx} | 89 +++-- ...value-container.js => value-container.tsx} | 11 +- .../src/show-error/index.js | 2 - .../src/show-error/{index.d.ts => index.ts} | 0 .../src/show-error/show-error.d.ts | 5 - .../src/show-error/show-error.js | 17 - .../src/show-error/show-error.ts | 22 ++ .../pf4-component-mapper/src/slider/index.js | 2 - .../src/slider/{index.d.ts => index.tsx} | 1 - .../src/slider/slider.d.ts | 12 - .../src/slider/{slider.js => slider.tsx} | 3 +- .../src/sub-form/index.js | 2 - .../src/sub-form/{index.d.ts => index.tsx} | 1 - .../src/sub-form/sub-form.d.ts | 13 - .../sub-form/{sub-form.js => sub-form.tsx} | 5 +- .../pf4-component-mapper/src/switch/index.js | 2 - .../src/switch/{index.d.ts => index.ts} | 0 .../src/switch/switch.d.ts | 13 - .../src/switch/{switch.js => switch.tsx} | 13 +- .../pf4-component-mapper/src/tabs/index.js | 2 - .../src/tabs/{index.d.ts => index.tsx} | 1 - .../pf4-component-mapper/src/tabs/tabs.d.ts | 17 - .../src/tabs/{tabs.js => tabs.tsx} | 15 +- .../src/tests/select/select.test.js | 35 +- .../src/text-field/index.js | 2 - .../src/text-field/{index.d.ts => index.ts} | 0 .../src/text-field/text-field.d.ts | 11 - .../{text-field.js => text-field.tsx} | 31 +- .../src/textarea/index.js | 2 - .../src/textarea/{index.d.ts => index.ts} | 0 .../src/textarea/textarea.d.ts | 11 - .../textarea/{textarea.js => textarea.tsx} | 10 +- .../src/time-picker/index.js | 2 - .../src/time-picker/{index.d.ts => index.tsx} | 1 - .../src/time-picker/time-picker.d.ts | 9 - .../{time-picker.js => time-picker.tsx} | 6 +- packages/pf4-component-mapper/src/types.ts | 349 ++++++++++++++++++ .../pf4-component-mapper/src/wizard/index.js | 2 - .../src/wizard/{index.d.ts => index.tsx} | 1 - .../{reducer.js => reducer.tsx} | 12 +- .../{step-buttons.js => step-buttons.tsx} | 46 ++- .../{wizard-nav.js => wizard-nav.tsx} | 42 ++- .../{wizard-step.js => wizard-step.tsx} | 45 ++- .../{wizard-toggle.js => wizard-toggle.tsx} | 5 +- .../src/wizard/wizard.d.ts | 101 ----- .../src/wizard/{wizard.js => wizard.tsx} | 75 +++- .../pf4-component-mapper/tsconfig.cjs.json | 9 + .../pf4-component-mapper/tsconfig.demo.json | 31 ++ .../pf4-component-mapper/tsconfig.esm.json | 9 + packages/pf4-component-mapper/tsconfig.json | 24 +- .../pf4-component-mapper/tsconfig.spec.json | 2 + 131 files changed, 1812 insertions(+), 1214 deletions(-) create mode 100644 packages/pf4-component-mapper/config/rspack.config.js delete mode 120000 packages/pf4-component-mapper/config/webpack.config.js rename packages/pf4-component-mapper/demo/demo-schemas/{dual-list-schema.js => dual-list-schema.tsx} (100%) rename packages/pf4-component-mapper/demo/demo-schemas/{sandbox.js => sandbox.ts} (77%) rename packages/pf4-component-mapper/demo/demo-schemas/{select-schema.js => select-schema.tsx} (81%) rename packages/pf4-component-mapper/demo/demo-schemas/{widget-schema.js => widget-schema.ts} (60%) rename packages/pf4-component-mapper/demo/demo-schemas/{wizard-schema.js => wizard-schema.tsx} (66%) rename packages/pf4-component-mapper/demo/{index.js => index.tsx} (85%) delete mode 100644 packages/pf4-component-mapper/src/checkbox/checkbox.d.ts rename packages/pf4-component-mapper/src/checkbox/{checkbox.js => checkbox.tsx} (61%) delete mode 100644 packages/pf4-component-mapper/src/checkbox/index.js rename packages/pf4-component-mapper/src/checkbox/{index.d.ts => index.ts} (100%) delete mode 100644 packages/pf4-component-mapper/src/checkbox/multiple-choice-list.js create mode 100644 packages/pf4-component-mapper/src/checkbox/multiple-choice-list.tsx delete mode 100644 packages/pf4-component-mapper/src/component-mapper/component-mapper.d.ts rename packages/pf4-component-mapper/src/component-mapper/{component-mapper.js => component-mapper.ts} (85%) delete mode 100644 packages/pf4-component-mapper/src/component-mapper/index.js rename packages/pf4-component-mapper/src/component-mapper/{index.d.ts => index.ts} (100%) delete mode 100644 packages/pf4-component-mapper/src/date-picker/date-picker.d.ts rename packages/pf4-component-mapper/src/date-picker/{date-picker.js => date-picker.tsx} (79%) delete mode 100644 packages/pf4-component-mapper/src/date-picker/index.js rename packages/pf4-component-mapper/src/date-picker/{index.d.ts => index.tsx} (56%) delete mode 100644 packages/pf4-component-mapper/src/dual-list-context/dual-list-context.d.ts rename packages/pf4-component-mapper/src/dual-list-context/{dual-list-context.js => dual-list-context.tsx} (60%) delete mode 100644 packages/pf4-component-mapper/src/dual-list-context/index.js rename packages/pf4-component-mapper/src/dual-list-context/{index.d.ts => index.tsx} (55%) delete mode 100644 packages/pf4-component-mapper/src/dual-list-select/dual-list-select.d.ts delete mode 100644 packages/pf4-component-mapper/src/dual-list-select/dual-list-select.js create mode 100644 packages/pf4-component-mapper/src/dual-list-select/dual-list-select.tsx delete mode 100644 packages/pf4-component-mapper/src/dual-list-select/index.js rename packages/pf4-component-mapper/src/dual-list-select/{index.d.ts => index.tsx} (56%) delete mode 100644 packages/pf4-component-mapper/src/dual-list-sort-button/dual-list-sort-button.d.ts rename packages/pf4-component-mapper/src/dual-list-sort-button/{dual-list-sort-button.js => dual-list-sort-button.tsx} (82%) delete mode 100644 packages/pf4-component-mapper/src/dual-list-sort-button/index.js rename packages/pf4-component-mapper/src/dual-list-sort-button/{index.d.ts => index.tsx} (55%) delete mode 100644 packages/pf4-component-mapper/src/dual-list-tree-select/dual-list-tree-select.d.ts rename packages/pf4-component-mapper/src/dual-list-tree-select/{dual-list-tree-select.js => dual-list-tree-select.tsx} (63%) delete mode 100644 packages/pf4-component-mapper/src/dual-list-tree-select/index.d.ts delete mode 100644 packages/pf4-component-mapper/src/dual-list-tree-select/index.js create mode 100644 packages/pf4-component-mapper/src/dual-list-tree-select/index.tsx delete mode 100644 packages/pf4-component-mapper/src/field-array/field-array.d.ts rename packages/pf4-component-mapper/src/field-array/{field-array.js => field-array.tsx} (73%) delete mode 100644 packages/pf4-component-mapper/src/field-array/index.js rename packages/pf4-component-mapper/src/field-array/{index.d.ts => index.tsx} (100%) delete mode 100644 packages/pf4-component-mapper/src/form-group/form-group.d.ts rename packages/pf4-component-mapper/src/form-group/{form-group.js => form-group.tsx} (66%) delete mode 100644 packages/pf4-component-mapper/src/form-group/index.js rename packages/pf4-component-mapper/src/form-group/{index.d.ts => index.ts} (100%) delete mode 100644 packages/pf4-component-mapper/src/form-template/form-template.d.ts rename packages/pf4-component-mapper/src/form-template/{form-template.js => form-template.tsx} (58%) delete mode 100644 packages/pf4-component-mapper/src/form-template/index.d.ts delete mode 100644 packages/pf4-component-mapper/src/form-template/index.js create mode 100644 packages/pf4-component-mapper/src/form-template/index.tsx rename packages/pf4-component-mapper/src/{index.js => index.ts} (84%) delete mode 100644 packages/pf4-component-mapper/src/is-required/index.js rename packages/pf4-component-mapper/src/is-required/{index.d.ts => index.ts} (100%) delete mode 100644 packages/pf4-component-mapper/src/is-required/is-required.d.ts rename packages/pf4-component-mapper/src/is-required/{is-required.js => is-required.tsx} (61%) delete mode 100644 packages/pf4-component-mapper/src/plain-text/index.js rename packages/pf4-component-mapper/src/plain-text/{index.d.ts => index.ts} (100%) delete mode 100644 packages/pf4-component-mapper/src/plain-text/plain-text.d.ts delete mode 100644 packages/pf4-component-mapper/src/plain-text/plain-text.js create mode 100644 packages/pf4-component-mapper/src/plain-text/plain-text.tsx delete mode 100644 packages/pf4-component-mapper/src/radio/index.js rename packages/pf4-component-mapper/src/radio/{index.d.ts => index.tsx} (58%) delete mode 100644 packages/pf4-component-mapper/src/radio/radio.d.ts rename packages/pf4-component-mapper/src/radio/{radio.js => radio.tsx} (74%) delete mode 100644 packages/pf4-component-mapper/src/select/index.js rename packages/pf4-component-mapper/src/select/{index.d.ts => index.ts} (100%) delete mode 100644 packages/pf4-component-mapper/src/select/select.d.ts rename packages/pf4-component-mapper/src/select/{select.js => select.tsx} (55%) rename packages/pf4-component-mapper/src/select/select/{clear-indicator.js => clear-indicator.tsx} (73%) rename packages/pf4-component-mapper/src/select/select/{empty-options.js => empty-options.tsx} (100%) rename packages/pf4-component-mapper/src/select/select/{input.js => input.tsx} (68%) rename packages/pf4-component-mapper/src/select/select/{menu.js => menu.tsx} (60%) rename packages/pf4-component-mapper/src/select/select/{option.js => option.tsx} (100%) rename packages/pf4-component-mapper/src/select/select/{select.js => select.tsx} (77%) rename packages/pf4-component-mapper/src/select/select/{value-container.js => value-container.tsx} (61%) delete mode 100644 packages/pf4-component-mapper/src/show-error/index.js rename packages/pf4-component-mapper/src/show-error/{index.d.ts => index.ts} (100%) delete mode 100644 packages/pf4-component-mapper/src/show-error/show-error.d.ts delete mode 100644 packages/pf4-component-mapper/src/show-error/show-error.js create mode 100644 packages/pf4-component-mapper/src/show-error/show-error.ts delete mode 100644 packages/pf4-component-mapper/src/slider/index.js rename packages/pf4-component-mapper/src/slider/{index.d.ts => index.tsx} (58%) delete mode 100644 packages/pf4-component-mapper/src/slider/slider.d.ts rename packages/pf4-component-mapper/src/slider/{slider.js => slider.tsx} (87%) delete mode 100644 packages/pf4-component-mapper/src/sub-form/index.js rename packages/pf4-component-mapper/src/sub-form/{index.d.ts => index.tsx} (57%) delete mode 100644 packages/pf4-component-mapper/src/sub-form/sub-form.d.ts rename packages/pf4-component-mapper/src/sub-form/{sub-form.js => sub-form.tsx} (75%) delete mode 100644 packages/pf4-component-mapper/src/switch/index.js rename packages/pf4-component-mapper/src/switch/{index.d.ts => index.ts} (100%) delete mode 100644 packages/pf4-component-mapper/src/switch/switch.d.ts rename packages/pf4-component-mapper/src/switch/{switch.js => switch.tsx} (70%) delete mode 100644 packages/pf4-component-mapper/src/tabs/index.js rename packages/pf4-component-mapper/src/tabs/{index.d.ts => index.tsx} (58%) delete mode 100644 packages/pf4-component-mapper/src/tabs/tabs.d.ts rename packages/pf4-component-mapper/src/tabs/{tabs.js => tabs.tsx} (55%) delete mode 100644 packages/pf4-component-mapper/src/text-field/index.js rename packages/pf4-component-mapper/src/text-field/{index.d.ts => index.ts} (100%) delete mode 100644 packages/pf4-component-mapper/src/text-field/text-field.d.ts rename packages/pf4-component-mapper/src/text-field/{text-field.js => text-field.tsx} (61%) delete mode 100644 packages/pf4-component-mapper/src/textarea/index.js rename packages/pf4-component-mapper/src/textarea/{index.d.ts => index.ts} (100%) delete mode 100644 packages/pf4-component-mapper/src/textarea/textarea.d.ts rename packages/pf4-component-mapper/src/textarea/{textarea.js => textarea.tsx} (72%) delete mode 100644 packages/pf4-component-mapper/src/time-picker/index.js rename packages/pf4-component-mapper/src/time-picker/{index.d.ts => index.tsx} (56%) delete mode 100644 packages/pf4-component-mapper/src/time-picker/time-picker.d.ts rename packages/pf4-component-mapper/src/time-picker/{time-picker.js => time-picker.tsx} (79%) create mode 100644 packages/pf4-component-mapper/src/types.ts delete mode 100644 packages/pf4-component-mapper/src/wizard/index.js rename packages/pf4-component-mapper/src/wizard/{index.d.ts => index.tsx} (58%) rename packages/pf4-component-mapper/src/wizard/wizard-components/{reducer.js => reducer.tsx} (62%) rename packages/pf4-component-mapper/src/wizard/wizard-components/{step-buttons.js => step-buttons.tsx} (73%) rename packages/pf4-component-mapper/src/wizard/wizard-components/{wizard-nav.js => wizard-nav.tsx} (71%) rename packages/pf4-component-mapper/src/wizard/wizard-components/{wizard-step.js => wizard-step.tsx} (57%) rename packages/pf4-component-mapper/src/wizard/wizard-components/{wizard-toggle.js => wizard-toggle.tsx} (80%) delete mode 100644 packages/pf4-component-mapper/src/wizard/wizard.d.ts rename packages/pf4-component-mapper/src/wizard/{wizard.js => wizard.tsx} (68%) create mode 100644 packages/pf4-component-mapper/tsconfig.cjs.json create mode 100644 packages/pf4-component-mapper/tsconfig.demo.json create mode 100644 packages/pf4-component-mapper/tsconfig.esm.json diff --git a/package.json b/package.json index 3e829fb41..fffbb8d59 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "start-demo": "nx run @data-driven-forms/react-renderer-demo:start", "lint": "npm run lint:js && npm run lint:typescript", "lint:js": "npx eslint ./packages/*/src --ext .js", - "lint:typescript": "eslint packages/react-form-renderer/src/**/* packages/react-form-renderer/demo/**/* --ext .ts,.tsx,.js,.jsx", + "lint:typescript": "eslint packages/react-form-renderer/src/**/* packages/react-form-renderer/demo/**/* packages/pf4-component-mapper/src/**/* packages/pf4-component-mapper/demo/**/* --ext .ts,.tsx,.js,.jsx", "generate-template": "node ./scripts/generate-mapper.js", "clean-build": "node ./scripts/clean-build.js", "prebuild": "node ./scripts/clean-build.js" diff --git a/packages/pf4-component-mapper/.gitignore b/packages/pf4-component-mapper/.gitignore index 53ae9421b..f38734f9b 100644 --- a/packages/pf4-component-mapper/.gitignore +++ b/packages/pf4-component-mapper/.gitignore @@ -77,6 +77,7 @@ vendor !src/* !demo/* !config +!config/rspack.config.js !.npmignore !.gitignore !.nxignore @@ -87,7 +88,10 @@ vendor !project.json !README.md !tsconfig.json +!tsconfig.cjs.json +!tsconfig.esm.json !tsconfig.spec.json +!tsconfig.demo.json !jest.config.ts !generate-typings.js !rollup.config.js diff --git a/packages/pf4-component-mapper/config/rspack.config.js b/packages/pf4-component-mapper/config/rspack.config.js new file mode 100644 index 000000000..f83f77b0c --- /dev/null +++ b/packages/pf4-component-mapper/config/rspack.config.js @@ -0,0 +1,90 @@ +//@ts-check + +const { defineConfig } = require('@rspack/cli'); +const { HtmlRspackPlugin, DefinePlugin, ProvidePlugin } = require('@rspack/core'); +const resolve = require('path').resolve; + +module.exports = defineConfig({ + mode: 'development', + entry: { app: resolve('./demo/index.tsx') }, + output: { + path: resolve('../dist'), + filename: '[name].[hash].js' + }, + devtool: 'eval-source-map', + resolve: { + extensions: ['.ts', '.tsx', '.js', '.jsx'], + }, + plugins: [ + new HtmlRspackPlugin({ + template: './demo/index.html', + filename: './index.html' + }), + new DefinePlugin({ + 'process.env.NODE_ENV': '"development"', + }), + new ProvidePlugin({ + process: 'process/browser.js' + }) + ], + devServer: { + port: 3001, // Different port from renderer to avoid conflicts + hot: true, + open: true, + }, + module: { + rules: [ + // TypeScript files with ts-loader for proper config support + { + test: /\.(ts|tsx)$/, + exclude: /node_modules/, + use: { + loader: 'ts-loader', + options: { + configFile: 'tsconfig.demo.json' + } + }, + }, + // JavaScript files (if any remain) + { + test: /\.js$/, + exclude: /(node_modules)/, + use: { + loader: 'builtin:swc-loader', + options: { + sourceMap: true, + jsc: { + parser: { + syntax: 'ecmascript', + jsx: true, + }, + transform: { + react: { + runtime: 'automatic', + }, + }, + }, + }, + }, + }, + // CSS/SCSS - using RSpack built-in support + { + test: /\.css$/, + type: 'css' + }, + { + test: /\.(sa|sc)ss$/, + use: ['sass-loader'], + type: 'css' + }, + // Assets + { + test: /\.(png|jpg|gif|svg|woff|ttf|eot)$/, + type: 'asset/resource' + }, + ] + }, + experiments: { + css: true, + } +}); \ No newline at end of file diff --git a/packages/pf4-component-mapper/config/webpack.config.js b/packages/pf4-component-mapper/config/webpack.config.js deleted file mode 120000 index bccde58a6..000000000 --- a/packages/pf4-component-mapper/config/webpack.config.js +++ /dev/null @@ -1 +0,0 @@ -../../common/config/webpack.config.js \ No newline at end of file diff --git a/packages/pf4-component-mapper/demo/demo-schemas/dual-list-schema.js b/packages/pf4-component-mapper/demo/demo-schemas/dual-list-schema.tsx similarity index 100% rename from packages/pf4-component-mapper/demo/demo-schemas/dual-list-schema.js rename to packages/pf4-component-mapper/demo/demo-schemas/dual-list-schema.tsx diff --git a/packages/pf4-component-mapper/demo/demo-schemas/sandbox.js b/packages/pf4-component-mapper/demo/demo-schemas/sandbox.ts similarity index 77% rename from packages/pf4-component-mapper/demo/demo-schemas/sandbox.js rename to packages/pf4-component-mapper/demo/demo-schemas/sandbox.ts index 782f52393..6c25f3f09 100644 --- a/packages/pf4-component-mapper/demo/demo-schemas/sandbox.js +++ b/packages/pf4-component-mapper/demo/demo-schemas/sandbox.ts @@ -6,12 +6,12 @@ const asyncOptions = [ { label: 'async-option-2', value: 'async-option-2' }, { label: 'async-option-3', value: 'async-option-3' }, { label: 'async option pepa 1', value: 'async-option-4' }, - { label: 'async option pepa 2', value: 'async-option-5' } + { label: 'async option pepa 2', value: 'async-option-5' }, ]; const baseOptions = asyncOptions.slice(0, 3); -const asyncLoadOptions = (searchValue) => +const asyncLoadOptions = (searchValue: string) => new Promise((resolve) => setTimeout(() => { if (searchValue && searchValue.trim() !== '') { @@ -42,26 +42,26 @@ const output = { onText: 'Switch is on', offText: 'Switch is off', title: 'Switch', - component: components.SWITCH + component: components.SWITCH, }, { name: 'switch_2', label: 'Switch disabled', component: components.SWITCH, - isDisabled: true + isDisabled: true, }, { name: 'switch_3', label: 'Switch readOnly', component: components.SWITCH, - isReadOnly: true + isReadOnly: true, }, { name: 'text_box_2', label: 'Text Box with help', title: 'Text Box with help', helperText: 'Helper text', - component: components.TEXT_FIELD + component: components.TEXT_FIELD, }, { name: 'text_box_3', @@ -69,26 +69,26 @@ const output = { title: 'Text Box required', isRequired: true, component: components.TEXT_FIELD, - validate: [{ type: validators.REQUIRED }] + validate: [{ type: validators.REQUIRED }], }, { name: 'text_box_4', label: 'Text Box readonly', title: 'Text Box readonly', isReadOnly: true, - component: components.TEXT_FIELD + component: components.TEXT_FIELD, }, { name: 'text_box_5', label: 'Text Box default', title: 'Text Box default', - component: components.TEXT_FIELD + component: components.TEXT_FIELD, }, { name: 'text_box_6', label: 'Text Box unvisible', title: 'Text Box unvisible', - component: components.TEXT_FIELD + component: components.TEXT_FIELD, }, { name: 'text_box_7', @@ -97,10 +97,10 @@ const output = { validate: [ { type: validators.PATTERN, - pattern: '[0-9]' - } + pattern: '[0-9]', + }, ], - component: components.TEXT_FIELD + component: components.TEXT_FIELD, }, { name: 'text_box_8', @@ -108,17 +108,17 @@ const output = { title: 'Text Box integer value', dataType: 'integer', component: components.TEXT_FIELD, - type: 'number' + type: 'number', }, { name: 'text_box_9', label: 'Text Box string value', title: 'Text Box string value', dataType: 'string', - component: components.TEXT_FIELD - } + component: components.TEXT_FIELD, + }, ], - component: components.SUB_FORM + component: components.SUB_FORM, }, { title: 'Text areas', @@ -128,12 +128,12 @@ const output = { name: 'textarea_box_1', label: 'Text Area', title: 'Text Area', - component: components.TEXTAREA - } + component: components.TEXTAREA, + }, ], - component: components.SUB_FORM - } - ] + component: components.SUB_FORM, + }, + ], }, { title: 'Tab 2', @@ -152,22 +152,22 @@ const output = { options: [ { value: 1, - label: 'Option 1' + label: 'Option 1', }, { value: 2, - label: 'Option 2' - } - ] + label: 'Option 2', + }, + ], }, { name: 'check_box_2', label: 'Check Box checked', title: 'Check Box checked', - component: components.CHECKBOX - } + component: components.CHECKBOX, + }, ], - component: components.SUB_FORM + component: components.SUB_FORM, }, { title: 'Radios', @@ -182,17 +182,17 @@ const output = { options: [ { label: 'One', - value: '1' + value: '1', }, { label: 'Two', - value: '2' + value: '2', }, { label: 'Three', - value: '3' - } - ] + value: '3', + }, + ], }, { name: 'radio_button_2', @@ -203,17 +203,17 @@ const output = { options: [ { label: 'One', - value: '1' + value: '1', }, { label: 'Two', - value: '2' + value: '2', }, { label: 'Three', - value: '3' - } - ] + value: '3', + }, + ], }, { name: 'radio_button_4', @@ -224,22 +224,22 @@ const output = { options: [ { label: 'One', - value: '1' + value: '1', }, { label: 'Two', - value: '2' + value: '2', }, { label: 'Three', - value: '3' - } - ] - } + value: '3', + }, + ], + }, ], - component: components.SUB_FORM - } - ] + component: components.SUB_FORM, + }, + ], }, { title: 'Tab 3', @@ -259,7 +259,7 @@ const output = { isClearable: true, isMulti: true, component: components.SELECT, - loadOptions: asyncLoadOptions + loadOptions: asyncLoadOptions, }, { name: 'dropdown_list_1', @@ -275,21 +275,21 @@ const output = { // eslint-disable-next-line max-len label: 'dropdown_list_1 dropdown_list_1 dropdown_list_1 dropdown_list_1 dropdown_list_1 dropdown_list_1 dropdown_list_1 dropdown_list_1 dropdown_list_1 dropdown_list_1 ', - value: 'foo' + value: 'foo', }, { label: 'One', - value: '1' + value: '1', }, { label: 'Three', - value: '3' + value: '3', }, { label: 'Two', - value: '2' - } - ] + value: '2', + }, + ], }, { name: 'dropdown_list_2', @@ -301,21 +301,21 @@ const output = { options: [ { label: '', - value: null + value: null, }, { label: 'One', - value: '1' + value: '1', }, { label: 'Three', - value: '3' + value: '3', }, { label: 'Two', - value: '2' - } - ] + value: '2', + }, + ], }, { name: 'dropdown_list_3', @@ -329,41 +329,41 @@ const output = { options: [ { label: '', - value: undefined + value: undefined, }, { label: 'One', - value: '1' + value: '1', }, { label: 'Three', - value: '3' + value: '3', }, { label: 'Two', - value: '2' + value: '2', }, { label: 'Four', - value: '4' + value: '4', }, { label: 'Five', - value: '5' + value: '5', }, { label: 'Six', - value: '6' + value: '6', }, { label: 'Seven', - value: '7' + value: '7', }, { label: 'Eight', - value: '8' - } - ] + value: '8', + }, + ], }, { name: 'dropdown_list_4', @@ -375,21 +375,21 @@ const output = { options: [ { label: '', - value: null + value: null, }, { label: 'One', - value: '1' + value: '1', }, { label: 'Two', - value: '2' + value: '2', }, { label: 'Three', - value: '3' - } - ] + value: '3', + }, + ], }, { name: 'dropdown_list_5', @@ -401,22 +401,22 @@ const output = { options: [ { label: 'One', - value: '1' + value: '1', }, { label: 'Two', - value: '2' + value: '2', }, { label: 'Three', - value: '3' - } - ] - } + value: '3', + }, + ], + }, ], - component: components.SUB_FORM - } - ] + component: components.SUB_FORM, + }, + ], }, { title: 'Tab 4', @@ -431,16 +431,16 @@ const output = { name: 'date_control_1', label: 'Datepicker', title: 'Datepicker', - component: components.DATE_PICKER + component: components.DATE_PICKER, }, { name: 'date_control_2', label: 'Datepicker with past days', title: 'Datepicker with past days', - component: components.DATE_PICKER - } + component: components.DATE_PICKER, + }, ], - component: components.SUB_FORM + component: components.SUB_FORM, }, { title: 'Timepickers', @@ -450,18 +450,18 @@ const output = { name: 'date_time_control_1', label: 'Timepicker', title: 'Timepicker', - component: components.TIME_PICKER + component: components.TIME_PICKER, }, { name: 'date_time_control_2', label: 'Timepicker with past days', title: 'Timepicker with past days', - component: components.TIME_PICKER - } + component: components.TIME_PICKER, + }, ], - component: components.SUB_FORM - } - ] + component: components.SUB_FORM, + }, + ], }, { title: 'Mixed', @@ -476,25 +476,25 @@ const output = { name: 'text_box_10', label: 'Text Box', title: 'Text Box', - component: components.TEXT_FIELD + component: components.TEXT_FIELD, }, { name: 'textarea_box_2', label: 'Text Area', title: 'Text Area', - component: components.TEXTAREA + component: components.TEXTAREA, }, { name: 'check_box_3', label: 'Check Box', title: 'Check Box', - component: components.CHECKBOX + component: components.CHECKBOX, }, { name: 'check_box_4', label: 'Check Box', title: 'Check Box', - component: components.CHECKBOX + component: components.CHECKBOX, }, { name: 'dropdown_list_5', @@ -505,21 +505,21 @@ const output = { options: [ { label: '', - value: null + value: null, }, { label: 'One', - value: '1' + value: '1', }, { label: 'Three', - value: '3' + value: '3', }, { label: 'Two', - value: '2' - } - ] + value: '2', + }, + ], }, { name: 'radio_button_3', @@ -530,41 +530,41 @@ const output = { options: [ { label: 'One', - value: '1' + value: '1', }, { label: 'Two', - value: '2' + value: '2', }, { label: 'Three', - value: '3' - } - ] + value: '3', + }, + ], }, { name: 'date_time_control_3', label: 'Timepicker', title: 'Timepicker', - component: components.TIME_PICKER - } + component: components.TIME_PICKER, + }, ], - component: components.SUB_FORM - } - ] - } + component: components.SUB_FORM, + }, + ], + }, ], component: components.TABS, - name: '57' - } - ] + name: '57', + }, + ], }; export const defaultValues = { text_box_5: '"hello"', check_box_2: 'true', radio_button_4: '2', - dropdown_list_2: '2' + dropdown_list_2: '2', }; export default output; diff --git a/packages/pf4-component-mapper/demo/demo-schemas/select-schema.js b/packages/pf4-component-mapper/demo/demo-schemas/select-schema.tsx similarity index 81% rename from packages/pf4-component-mapper/demo/demo-schemas/select-schema.js rename to packages/pf4-component-mapper/demo/demo-schemas/select-schema.tsx index 857087e5a..d782d7dcb 100644 --- a/packages/pf4-component-mapper/demo/demo-schemas/select-schema.js +++ b/packages/pf4-component-mapper/demo/demo-schemas/select-schema.tsx @@ -1,7 +1,14 @@ import React from 'react'; -import componentTypes from '@data-driven-forms/react-form-renderer/component-types'; +import { componentTypes } from '@data-driven-forms/react-form-renderer'; -const options = [ +interface SelectOption { + label: string | React.ReactNode; + value?: any; + key?: string; + isDisabled?: boolean; +} + +const options: SelectOption[] = [ { label: 'Morton', value: 'Jenifer', @@ -24,27 +31,27 @@ const options = [ }, ]; -const loadOptions = (inputValue = '') => { +const loadOptions = (inputValue: string = ''): Promise => { return new Promise((res) => setTimeout(() => { if (inputValue.length === 0) { return res(options.slice(0, 3)); } - return res(options.filter(({ label }) => label.toLocaleLowerCase().includes(inputValue.toLocaleLowerCase()))); + return res(options.filter(({ label }) => typeof label === 'string' && label.toLocaleLowerCase().includes(inputValue.toLocaleLowerCase()))); }, 1500) ); }; -const loadOptionsLong = (inputValue = '') => { - const options = [...Array(99)].map((_v, index) => ({ label: `${index}`, value: { index } })); +const loadOptionsLong = (inputValue: string = ''): Promise => { + const longOptions: SelectOption[] = [...Array(99)].map((_v, index) => ({ label: `${index}`, value: { index } })); return new Promise((res) => setTimeout(() => { if (inputValue.length === 0) { - return res(options.slice(0, 80)); + return res(longOptions.slice(0, 80)); } - return res(options.filter(({ label }) => label.toLocaleLowerCase().includes(inputValue.toLocaleLowerCase()))); + return res(longOptions.filter(({ label }) => typeof label === 'string' && label.toLocaleLowerCase().includes(inputValue.toLocaleLowerCase()))); }, 1500) ); }; @@ -56,7 +63,7 @@ const selectSchema = { name: 'long-searchable-async-select', label: 'Long searchable async select', loadOptions: loadOptionsLong, - compareValues: (a, b) => { + compareValues: (a: any, b: any) => { console.log('Custom compare is beeing used.'); return a.value === b.value; }, diff --git a/packages/pf4-component-mapper/demo/demo-schemas/widget-schema.js b/packages/pf4-component-mapper/demo/demo-schemas/widget-schema.ts similarity index 60% rename from packages/pf4-component-mapper/demo/demo-schemas/widget-schema.js rename to packages/pf4-component-mapper/demo/demo-schemas/widget-schema.ts index c56cb5496..765e164ec 100644 --- a/packages/pf4-component-mapper/demo/demo-schemas/widget-schema.js +++ b/packages/pf4-component-mapper/demo/demo-schemas/widget-schema.ts @@ -1,4 +1,38 @@ -export const arraySchemaDDF = { +interface ValidationRule { + type: string; + threshold?: number; +} + +interface Field { + component: string; + name?: string; + label?: string; + placeholder?: string; + isRequired?: boolean; + validate?: ValidationRule[]; + type?: string; +} + +interface FieldArrayField { + component: string; + name: string; + fieldKey?: string; + label: string; + description?: string; + itemDefault?: Record; + defaultItem?: any; + fields: Field[]; + validate?: ValidationRule[]; + minItems?: number; + maxItems?: number; +} + +interface Schema { + title: string; + fields: FieldArrayField[]; +} + +export const arraySchemaDDF: Schema = { title: 'FieldArray', fields: [ { @@ -17,17 +51,17 @@ export const arraySchemaDDF = { isRequired: true, validate: [ { - type: 'required' - } - ] + type: 'required', + }, + ], }, { component: 'text-field', name: 'lastName', label: 'Last Name', - placeholder: 'Stavitel' - } - ] + placeholder: 'Stavitel', + }, + ], }, { component: 'field-array', @@ -37,9 +71,9 @@ export const arraySchemaDDF = { fields: [ { component: 'text-field', - label: 'Item' - } - ] + label: 'Item', + }, + ], }, { component: 'field-array', @@ -50,9 +84,9 @@ export const arraySchemaDDF = { { component: 'text-field', label: 'Item', - type: 'number' - } - ] + type: 'number', + }, + ], }, { component: 'field-array', @@ -66,11 +100,11 @@ export const arraySchemaDDF = { isRequired: true, validate: [ { - type: 'required' - } - ] - } - ] - } - ] + type: 'required', + }, + ], + }, + ], + }, + ], }; diff --git a/packages/pf4-component-mapper/demo/demo-schemas/wizard-schema.js b/packages/pf4-component-mapper/demo/demo-schemas/wizard-schema.tsx similarity index 66% rename from packages/pf4-component-mapper/demo/demo-schemas/wizard-schema.js rename to packages/pf4-component-mapper/demo/demo-schemas/wizard-schema.tsx index e108d86d8..e3aeef8b4 100644 --- a/packages/pf4-component-mapper/demo/demo-schemas/wizard-schema.js +++ b/packages/pf4-component-mapper/demo/demo-schemas/wizard-schema.tsx @@ -1,11 +1,22 @@ import React, { useState } from 'react'; -import PropTypes from 'prop-types'; import { componentTypes, validatorTypes } from '@data-driven-forms/react-form-renderer'; import { Button } from '@patternfly/react-core'; import { useFormApi } from '@data-driven-forms/react-form-renderer'; -const ValidateButtons = ({ disableBack, handlePrev, buttonLabels: { back, cancel }, renderNextButton }) => { - const [state, setState] = useState('init'); +interface ButtonLabels { + back: React.ReactNode; + cancel: React.ReactNode; +} + +interface ValidateButtonsProps { + disableBack: boolean; + handlePrev: () => void; + buttonLabels: ButtonLabels; + renderNextButton: () => React.ReactNode; +} + +const ValidateButtons: React.FC = ({ disableBack, handlePrev, buttonLabels: { back, cancel }, renderNextButton }) => { + const [state, setState] = useState<'init' | 'validating' | 'done'>('init'); const formOptions = useFormApi(); const setValidating = () => { @@ -36,16 +47,65 @@ const ValidateButtons = ({ disableBack, handlePrev, buttonLabels: { back, cancel ); }; -ValidateButtons.propTypes = { - disableBack: PropTypes.bool, - handlePrev: PropTypes.func.isRequired, - buttonLabels: PropTypes.shape({ back: PropTypes.node.isRequired, cancel: PropTypes.node.isRequired }).isRequired, - renderNextButton: PropTypes.func.isRequired -}; +interface ValidationRule { + type: string; +} + +interface SelectOption { + label: string; + value?: any; + isDisabled?: boolean; +} + +interface BaseField { + component: string; + name: string; + label?: string; + type?: string; + validate?: ValidationRule[]; + isRequired?: boolean; + options?: SelectOption[]; +} + +interface StepMapper { + [key: string]: string; +} -const asyncValidator = () => new Promise((res) => setTimeout(() => res(), 1000)); +interface NextStepCondition { + when: string; + stepMapper: StepMapper; +} -export const wizardSchema = { +interface WizardStep { + title?: string; + name: string | number; + nextStep?: string | NextStepCondition | ((args: { values: any }) => string | undefined); + substepOf?: string; + fields: BaseField[]; + buttons?: React.ComponentType; + showTitle?: boolean; + disableForwardJumping?: boolean; + isProgressAfterSubmissionStep?: boolean; +} + +interface WizardField { + component: string; + name: string; + crossroads?: string[]; + title?: string; + showTitles?: boolean; + description?: string; + fields: WizardStep[]; + isDynamic?: boolean; +} + +interface Schema { + fields: WizardField[]; +} + +const asyncValidator = (): Promise => new Promise((res) => setTimeout(() => res(), 1000)); + +export const wizardSchema: Schema = { fields: [ { component: componentTypes.WIZARD, @@ -63,8 +123,8 @@ export const wizardSchema = { when: 'source.source-type', stepMapper: { aws: 'aws', - google: 'google' - } + google: 'google', + }, }, fields: [ { @@ -72,7 +132,7 @@ export const wizardSchema = { name: 'source.source-name', type: 'text', label: 'Source name', - validate: [asyncValidator] + validate: [asyncValidator as any], }, { component: componentTypes.SELECT, @@ -81,29 +141,29 @@ export const wizardSchema = { isRequired: true, options: [ { - label: 'Please Choose' + label: 'Please Choose', }, { value: 'aws', - label: 'Aws' + label: 'Aws', }, { value: 'google', - label: 'Google' + label: 'Google', }, { value: 'disabled', label: 'i am disabled', - isDisabled: true - } + isDisabled: true, + }, ], validate: [ { - type: validatorTypes.REQUIRED - } - ] - } - ] + type: validatorTypes.REQUIRED, + }, + ], + }, + ], }, { title: 'Configure AWS', @@ -118,12 +178,12 @@ export const wizardSchema = { label: 'Aws field part', validate: [ { - type: validatorTypes.REQUIRED - } + type: validatorTypes.REQUIRED, + }, ], - isRequired: true - } - ] + isRequired: true, + }, + ], }, { name: 'google', @@ -137,29 +197,29 @@ export const wizardSchema = { label: 'Google field part', validate: [ { - type: validatorTypes.REQUIRED - } - ] - } - ] + type: validatorTypes.REQUIRED, + }, + ], + }, + ], }, { fields: [ { name: 'summary', - component: 'summary' - } + component: 'summary', + }, ], name: 'summary', substepOf: 'Summary', - title: 'Summary' - } - ] - } - ] + title: 'Summary', + }, + ], + }, + ], }; -export const wizardSchemaWithFunction = { +export const wizardSchemaWithFunction: Schema = { fields: [ { component: componentTypes.WIZARD, @@ -172,13 +232,13 @@ export const wizardSchemaWithFunction = { { title: 'Get started with adding source', name: 1, - nextStep: ({ values }) => values.source && values.source['source-type'], + nextStep: ({ values }: { values: any }) => values.source && values.source['source-type'], fields: [ { component: componentTypes.TEXTAREA, name: 'source.source-name', type: 'text', - label: 'Source name' + label: 'Source name', }, { component: componentTypes.SELECT, @@ -187,24 +247,24 @@ export const wizardSchemaWithFunction = { isRequired: true, options: [ { - label: 'Please Choose' + label: 'Please Choose', }, { value: 'aws', - label: 'Aws' + label: 'Aws', }, { value: 'google', - label: 'Google' - } + label: 'Google', + }, ], validate: [ { - type: validatorTypes.REQUIRED - } - ] - } - ] + type: validatorTypes.REQUIRED, + }, + ], + }, + ], }, { title: 'Configure AWS', @@ -219,12 +279,12 @@ export const wizardSchemaWithFunction = { label: 'Aws field part', validate: [ { - type: validatorTypes.REQUIRED - } + type: validatorTypes.REQUIRED, + }, ], - isRequired: true - } - ] + isRequired: true, + }, + ], }, { name: 'google', @@ -238,29 +298,29 @@ export const wizardSchemaWithFunction = { label: 'Google field part', validate: [ { - type: validatorTypes.REQUIRED - } - ] - } - ] + type: validatorTypes.REQUIRED, + }, + ], + }, + ], }, { fields: [ { name: 'summary', - component: 'summary' - } + component: 'summary', + }, ], name: 'summary', substepOf: 'Summary', - title: 'Summary' - } - ] - } - ] + title: 'Summary', + }, + ], + }, + ], }; -export const wizardSchemaSimple = { +export const wizardSchemaSimple: Schema = { fields: [ { component: componentTypes.WIZARD, @@ -277,9 +337,9 @@ export const wizardSchemaSimple = { component: componentTypes.TEXTAREA, name: 'source-name', type: 'text', - label: 'Source name' - } - ] + label: 'Source name', + }, + ], }, { title: 'Configure AWS', @@ -293,28 +353,28 @@ export const wizardSchemaSimple = { isRequired: true, validate: [ { - type: validatorTypes.REQUIRED - } - ] - } - ] + type: validatorTypes.REQUIRED, + }, + ], + }, + ], }, { fields: [ { name: 'summary', - component: 'summary' - } + component: 'summary', + }, ], name: 'summary', - title: 'Summary' - } - ] - } - ] + title: 'Summary', + }, + ], + }, + ], }; -export const wizardSchemaSubsteps = { +export const wizardSchemaSubsteps: Schema = { fields: [ { component: componentTypes.WIZARD, @@ -332,9 +392,9 @@ export const wizardSchemaSubsteps = { component: componentTypes.TEXTAREA, name: 'source-name', type: 'text', - label: 'Source name' - } - ] + label: 'Source name', + }, + ], }, { title: 'Configure AWS', @@ -345,27 +405,27 @@ export const wizardSchemaSubsteps = { { component: componentTypes.TEXT_FIELD, name: 'aws-field', - label: 'Aws field part' - } - ] + label: 'Aws field part', + }, + ], }, { fields: [ { name: 'summary', - component: 'summary' - } + component: 'summary', + }, ], name: 'summary', title: 'Summary', - substepOf: 'Summary' - } - ] - } - ] + substepOf: 'Summary', + }, + ], + }, + ], }; -export const wizardSchemaMoreSubsteps = { +export const wizardSchemaMoreSubsteps: Schema = { fields: [ { component: componentTypes.WIZARD, @@ -383,9 +443,9 @@ export const wizardSchemaMoreSubsteps = { component: componentTypes.TEXTAREA, name: 'source-name', type: 'text', - label: 'Source name' - } - ] + label: 'Source name', + }, + ], }, { title: 'Configure AWS', @@ -396,9 +456,9 @@ export const wizardSchemaMoreSubsteps = { { component: componentTypes.TEXT_FIELD, name: 'aws-field', - label: 'Aws field part' - } - ] + label: 'Aws field part', + }, + ], }, { title: 'Configure AWS part 2 - disabled jumping', @@ -410,39 +470,39 @@ export const wizardSchemaMoreSubsteps = { { component: componentTypes.TEXT_FIELD, name: 'aws-field-1', - label: 'Aws field part 1' - } - ] + label: 'Aws field part 1', + }, + ], }, { fields: [ { name: 'summary', - component: 'summary' - } + component: 'summary', + }, ], name: 'summary', title: 'Summary', substepOf: 'Finish', - nextStep: 'summary2' + nextStep: 'summary2', }, { fields: [ { name: 'summary', - component: 'summary' - } + component: 'summary', + }, ], name: 'summary2', title: 'Summary2', - substepOf: 'Finish' - } - ] - } - ] + substepOf: 'Finish', + }, + ], + }, + ], }; -export const wizardSchemaProgressAfterSubmission = { +export const wizardSchemaProgressAfterSubmission: Schema = { fields: [ { component: componentTypes.WIZARD, @@ -462,11 +522,11 @@ export const wizardSchemaProgressAfterSubmission = { isRequired: true, validate: [ { - type: validatorTypes.REQUIRED - } - ] - } - ] + type: validatorTypes.REQUIRED, + }, + ], + }, + ], }, { title: 'Step 2', @@ -480,11 +540,11 @@ export const wizardSchemaProgressAfterSubmission = { isRequired: true, validate: [ { - type: validatorTypes.REQUIRED - } - ] - } - ] + type: validatorTypes.REQUIRED, + }, + ], + }, + ], }, { name: 'progress-step', @@ -492,11 +552,11 @@ export const wizardSchemaProgressAfterSubmission = { fields: [ { name: 'progress-content', - component: 'progress-step-content' - } - ] - } - ] - } - ] + component: 'progress-step-content', + }, + ], + }, + ], + }, + ], }; diff --git a/packages/pf4-component-mapper/demo/index.js b/packages/pf4-component-mapper/demo/index.tsx similarity index 85% rename from packages/pf4-component-mapper/demo/index.js rename to packages/pf4-component-mapper/demo/index.tsx index b23969783..9102346bf 100644 --- a/packages/pf4-component-mapper/demo/index.js +++ b/packages/pf4-component-mapper/demo/index.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { createRoot } from 'react-dom/client'; import { FormRenderer, WizardContext } from '@data-driven-forms/react-form-renderer'; import { arraySchemaDDF } from './demo-schemas/widget-schema'; @@ -29,8 +29,9 @@ import sandboxSchema from './demo-schemas/sandbox'; import dualSchema from './demo-schemas/dual-list-schema'; import demoSchema from '../../../shared/demoschema'; import selectSchema from './demo-schemas/select-schema'; +import Select from '../src/select/select/select'; -const Summary = (props) =>
Custom summary component.
; +const Summary = (props: any) =>
Custom summary component.
; const ProgressStepContent = () => { const { jumpToStep } = React.useContext(WizardContext); @@ -84,15 +85,21 @@ const fieldArrayState = { }, }; -class App extends React.Component { - constructor(props) { +interface AppState { + schema: any; + additionalOptions: any; + ui?: any; +} + +class App extends React.Component<{}, AppState> { + constructor(props: {}) { super(props); this.state = { schema: wizardSchema, additionalOptions: {} }; } render() { return ( -
+
Pf4 component mapper @@ -213,5 +220,45 @@ class App extends React.Component { } const container = document.getElementById('root'); -const root = createRoot(container); +const root = createRoot(container!); + +const NEW_OPTIONS = [{ label: 'Different label', value: 2 }]; +const asyncLoadingNew = () => Promise.resolve(NEW_OPTIONS); + +function SelectApp() { + const onChange = (value: any) => { + console.log('Selected:', value); + } + const initialProps = { + onChange, + name: 'test-select', + id: 'select', + options: [ + { + label: 'First option', + value: 1, + }, + { + label: 'Second option', + value: 2, + }, + ], + }; + const asyncLoading = () => Promise.resolve([{ label: 'labelxxx', value: '123' }]); + + const [al, setAl] = useState(() => asyncLoading); + return ( + <Select + {...initialProps} + value={[{ value: '123', label: 'labelxxx' }, 'Not in options']} + isMulti + options={undefined} + loadOptions={asyncLoading} + onChange={onChange} + simpleValue + /> + ) +} + + root.render(<App />); diff --git a/packages/pf4-component-mapper/jest.config.ts b/packages/pf4-component-mapper/jest.config.ts index 1fe4739c9..e2dd903ef 100644 --- a/packages/pf4-component-mapper/jest.config.ts +++ b/packages/pf4-component-mapper/jest.config.ts @@ -1,6 +1,12 @@ export default { displayName: '@data-driven-forms/pf4-component-mapper', preset: '../../jest.preset.js', + transform: { + '^.+\\.[tj]sx?$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }], + }, + transformIgnorePatterns: [ + 'node_modules/(?!(@testing-library|@data-driven-forms)/)', + ], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], coverageDirectory: '../../coverage/packages/pf4-component-mapper', }; \ No newline at end of file diff --git a/packages/pf4-component-mapper/package.json b/packages/pf4-component-mapper/package.json index ede454bb8..b4a79a476 100644 --- a/packages/pf4-component-mapper/package.json +++ b/packages/pf4-component-mapper/package.json @@ -7,11 +7,10 @@ "typings": "index.d.ts", "license": "Apache-2.0", "scripts": { - "start": "webpack-dev-server --config ./config/webpack.config.js --open --hot", - "build": "npm run build:cjs && npm run build:esm && npm run build:typings && npm run build:packages && npm run build:css", - "build:cjs": "BABEL_ENV=cjs babel src --out-dir ./ --ignore \"src/tests/*\"", - "build:esm": "BABEL_ENV=esm babel src --out-dir ./esm --ignore \"src/tests/*\"", - "build:typings": "node ../../scripts/generate-typings.js", + "start": "rspack serve --config ./config/rspack.config.js", + "build": "npm run build:cjs && npm run build:esm && npm run build:packages && npm run build:css", + "build:cjs": "tsc -p tsconfig.cjs.json", + "build:esm": "tsc -p tsconfig.esm.json", "build:packages": "node ../../scripts/generate-packages.js", "build:css": "node ../../scripts/copy-css.js" }, diff --git a/packages/pf4-component-mapper/src/checkbox/checkbox.d.ts b/packages/pf4-component-mapper/src/checkbox/checkbox.d.ts deleted file mode 100644 index af95f1990..000000000 --- a/packages/pf4-component-mapper/src/checkbox/checkbox.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { UseFieldApiComponentConfig, AnyObject } from "@data-driven-forms/react-form-renderer"; -import { CheckboxProps as PfCheckboxProps } from '@patternfly/react-core'; -import { ReactNode } from "react"; -import FormGroupProps from "../form-group"; - -interface CheckboxOptions extends AnyObject { - label?: ReactNode; - value?: any; -} - -interface InternalCheckboxProps extends PfCheckboxProps { - isReadOnly?: boolean; - options?: CheckboxOptions; -} - -export type CheckboxProps = InternalCheckboxProps & FormGroupProps & UseFieldApiComponentConfig; - -declare const Checkbox: React.ComponentType<CheckboxProps>; - -export default Checkbox; diff --git a/packages/pf4-component-mapper/src/checkbox/checkbox.js b/packages/pf4-component-mapper/src/checkbox/checkbox.tsx similarity index 61% rename from packages/pf4-component-mapper/src/checkbox/checkbox.js rename to packages/pf4-component-mapper/src/checkbox/checkbox.tsx index d9f4eaf23..ea69c63f4 100644 --- a/packages/pf4-component-mapper/src/checkbox/checkbox.js +++ b/packages/pf4-component-mapper/src/checkbox/checkbox.tsx @@ -2,12 +2,18 @@ import React from 'react'; import MultipleChoiceList from './multiple-choice-list'; import { useFieldApi } from '@data-driven-forms/react-form-renderer'; import FormGroup from '../form-group/form-group'; -import { Checkbox as Pf4Checkbox } from '@patternfly/react-core'; +import { Checkbox as Pf4Checkbox, CheckboxProps } from '@patternfly/react-core'; import IsRequired from '../is-required/is-required'; +import { BaseFieldProps, SelectOption } from '../types'; -const SingleCheckbox = (props) => { +export interface CheckboxFieldProps extends BaseFieldProps, Omit<CheckboxProps, keyof BaseFieldProps | 'id' | 'isChecked' | 'onChange'> { + options?: SelectOption[]; +} + +const SingleCheckbox: React.FC<CheckboxFieldProps> = (props) => { const { label, isRequired, helperText, meta, validateOnMount, description, input, isReadOnly, isDisabled, id, FormGroupProps, ...rest } = useFieldApi(props); + return ( <FormGroup isRequired={isRequired} @@ -32,6 +38,10 @@ const SingleCheckbox = (props) => { ); }; -const Checkbox = ({ options, ...props }) => (options ? <MultipleChoiceList options={options} {...props} /> : <SingleCheckbox {...props} />); +const Checkbox: React.FC<CheckboxFieldProps> = ({ options, ...props }) => + options ? <MultipleChoiceList options={options} {...props} /> : <SingleCheckbox {...props} />; export default Checkbox; + +// Export the props type for external use +export type { CheckboxProps } from '../types'; diff --git a/packages/pf4-component-mapper/src/checkbox/index.js b/packages/pf4-component-mapper/src/checkbox/index.js deleted file mode 100644 index 3f0e00717..000000000 --- a/packages/pf4-component-mapper/src/checkbox/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './checkbox'; -export * from './checkbox'; diff --git a/packages/pf4-component-mapper/src/checkbox/index.d.ts b/packages/pf4-component-mapper/src/checkbox/index.ts similarity index 100% rename from packages/pf4-component-mapper/src/checkbox/index.d.ts rename to packages/pf4-component-mapper/src/checkbox/index.ts diff --git a/packages/pf4-component-mapper/src/checkbox/multiple-choice-list.js b/packages/pf4-component-mapper/src/checkbox/multiple-choice-list.js deleted file mode 100644 index 7c4c21391..000000000 --- a/packages/pf4-component-mapper/src/checkbox/multiple-choice-list.js +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; - -import { Checkbox } from '@patternfly/react-core'; - -import MultipleChoiceListCommon from '@data-driven-forms/common/multiple-choice-list'; -import FormGroup from '../form-group/form-group'; - -const FinalCheckbox = ({ option, ...props }) => ( - <Checkbox isChecked={props.checked} {...props} onChange={(e, _value) => props.onChange(e)} {...option} /> -); - -const Wrapper = ({ meta, children, ...rest }) => ( - <FormGroup {...rest} id={rest.name || rest.id} meta={meta}> - {children} - </FormGroup> -); - -const MultipleChoiceList = (props) => <MultipleChoiceListCommon {...props} Wrapper={Wrapper} Checkbox={FinalCheckbox} />; - -export default MultipleChoiceList; diff --git a/packages/pf4-component-mapper/src/checkbox/multiple-choice-list.tsx b/packages/pf4-component-mapper/src/checkbox/multiple-choice-list.tsx new file mode 100644 index 000000000..706254662 --- /dev/null +++ b/packages/pf4-component-mapper/src/checkbox/multiple-choice-list.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { Checkbox } from '@patternfly/react-core'; +import MultipleChoiceListCommon from '@data-driven-forms/common/multiple-choice-list'; +import FormGroup from '../form-group/form-group'; +import { SelectOption, FieldMeta } from '../types'; + +interface FinalCheckboxProps { + value: any; + label: string; + name: string; + checked?: boolean; + onChange?: (value: any) => void; + 'aria-label'?: string; + id?: string; + isDisabled?: boolean; + option?: SelectOption; + [key: string]: any; +} + +const FinalCheckbox: React.FC<FinalCheckboxProps> = ({ value, label, name, checked, onChange, option, ...props }) => ( + <Checkbox + isChecked={checked} + {...props} + onChange={(e, _value) => onChange?.(value)} + label={label} + name={name} + id={props.id || `checkbox-${value || label}`} + /> +); + +interface WrapperProps { + meta: FieldMeta; + children: React.ReactNode; + name?: string; + id?: string; + [key: string]: any; +} + +const Wrapper: React.FC<WrapperProps> = ({ meta, children, ...rest }) => ( + <FormGroup {...rest} id={rest.name || rest.id} meta={meta}> + {children} + </FormGroup> +); + +export interface MultipleChoiceListProps { + options: SelectOption[]; + name?: string; + component?: string; + [key: string]: any; +} + +const MultipleChoiceList: React.FC<MultipleChoiceListProps> = ({ options, name, component, ...rest }) => ( + <MultipleChoiceListCommon {...rest} options={options} name={name} component={component} Wrapper={Wrapper} Checkbox={FinalCheckbox} /> +); + +export default MultipleChoiceList; diff --git a/packages/pf4-component-mapper/src/component-mapper/component-mapper.d.ts b/packages/pf4-component-mapper/src/component-mapper/component-mapper.d.ts deleted file mode 100644 index a153a174b..000000000 --- a/packages/pf4-component-mapper/src/component-mapper/component-mapper.d.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { ComponentTypes, ComponentMapper } from '@data-driven-forms/react-form-renderer'; - -interface Components { - TextField: React.ComponentType; - Textarea: React.ComponentType; - Select: React.ComponentType; - Checkbox: React.ComponentType; - Radio: React.ComponentType; - Switch: React.ComponentType; - DatePicker: React.ComponentType; - TimePicker: React.ComponentType; - PlainText: React.ComponentType; - SubForm: React.ComponentType; - Wizard: React.ComponentType; - DualListSelect: React.ComponentType; - Slider: React.ComponentType; -} - -interface componentMapper extends ComponentMapper { - [ComponentTypes.TEXT_FIELD]: React.ComponentType; - [ComponentTypes.TEXTAREA]: React.ComponentType; - [ComponentTypes.SELECT]: React.ComponentType; - [ComponentTypes.CHECKBOX]: React.ComponentType; - [ComponentTypes.SUB_FORM]: React.ComponentType; - [ComponentTypes.RADIO]: React.ComponentType; - [ComponentTypes.TABS]: React.ComponentType; - [ComponentTypes.DATE_PICKER]: React.ComponentType; - [ComponentTypes.TIME_PICKER]: React.ComponentType; - [ComponentTypes.SWITCH]: React.ComponentType; - [ComponentTypes.PLAIN_TEXT]: React.ComponentType; - [ComponentTypes.WIZARD]: React.ComponentType; - [ComponentTypes.FIELD_ARRAY]: React.ComponentType; - [ComponentTypes.DUAL_LIST_SELECT]: React.ComponentType; - [ComponentTypes.SLIDER]: React.ComponentType; -} - -interface RawComponents { - RawSelect: React.ComponentType; -} - -export const rawComponents: RawComponents; - -declare const componentMapper: componentMapper; - -export const components: Components; - -export default componentMapper; diff --git a/packages/pf4-component-mapper/src/component-mapper/component-mapper.js b/packages/pf4-component-mapper/src/component-mapper/component-mapper.ts similarity index 85% rename from packages/pf4-component-mapper/src/component-mapper/component-mapper.js rename to packages/pf4-component-mapper/src/component-mapper/component-mapper.ts index ef08e2b76..45ce5e17b 100644 --- a/packages/pf4-component-mapper/src/component-mapper/component-mapper.js +++ b/packages/pf4-component-mapper/src/component-mapper/component-mapper.ts @@ -1,22 +1,25 @@ -import { componentTypes } from '@data-driven-forms/react-form-renderer'; +import { ComponentMapper, componentTypes } from '@data-driven-forms/react-form-renderer'; + +// Import TypeScript components where available +import TextField from '../text-field'; +import Textarea from '../textarea'; +import Switch from '../switch'; +import PlainText from '../plain-text'; +import Checkbox from '../checkbox'; +import Select from '../select'; +// Needs nested import to get the original implementation without the HOC +import RawSelect from '../select/select/select'; import Tabs from '../tabs'; import SubForm from '../sub-form'; import Wizard from '../wizard'; -import Select from '../select'; -import RawSelect from '../select/select/select'; import FieldArray from '../field-array'; -import TextField from '../text-field'; -import Textarea from '../textarea'; -import Checkbox from '../checkbox'; import Radio from '../radio'; import DatePicker from '../date-picker'; import TimePicker from '../time-picker'; -import Switch from '../switch'; -import PlainText from '../plain-text'; import DualListSelect from '../dual-list-select'; import Slider from '../slider'; -const mapper = { +const mapper: ComponentMapper = { [componentTypes.TEXT_FIELD]: TextField, [componentTypes.TEXTAREA]: Textarea, [componentTypes.SELECT]: Select, diff --git a/packages/pf4-component-mapper/src/component-mapper/index.js b/packages/pf4-component-mapper/src/component-mapper/index.js deleted file mode 100644 index 0e792fa43..000000000 --- a/packages/pf4-component-mapper/src/component-mapper/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './component-mapper'; -export * from './component-mapper'; diff --git a/packages/pf4-component-mapper/src/component-mapper/index.d.ts b/packages/pf4-component-mapper/src/component-mapper/index.ts similarity index 100% rename from packages/pf4-component-mapper/src/component-mapper/index.d.ts rename to packages/pf4-component-mapper/src/component-mapper/index.ts diff --git a/packages/pf4-component-mapper/src/date-picker/date-picker.d.ts b/packages/pf4-component-mapper/src/date-picker/date-picker.d.ts deleted file mode 100644 index e2a763334..000000000 --- a/packages/pf4-component-mapper/src/date-picker/date-picker.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { UseFieldApiComponentConfig } from "@data-driven-forms/react-form-renderer"; -import { TextInputProps } from "@patternfly/react-core"; -import FormGroupProps from "../form-group"; - -export type DatePickerProps = TextInputProps & FormGroupProps & UseFieldApiComponentConfig; - -declare const DatePicker: React.ComponentType<DatePickerProps>; - -export default DatePicker; diff --git a/packages/pf4-component-mapper/src/date-picker/date-picker.js b/packages/pf4-component-mapper/src/date-picker/date-picker.tsx similarity index 79% rename from packages/pf4-component-mapper/src/date-picker/date-picker.js rename to packages/pf4-component-mapper/src/date-picker/date-picker.tsx index 011fe4248..5c83e4273 100644 --- a/packages/pf4-component-mapper/src/date-picker/date-picker.js +++ b/packages/pf4-component-mapper/src/date-picker/date-picker.tsx @@ -2,10 +2,12 @@ import React from 'react'; import { DatePicker as PF4DatePicker } from '@patternfly/react-core'; import { useFieldApi } from '@data-driven-forms/react-form-renderer'; import FormGroup from '../form-group/form-group'; +import { BaseFieldProps, DatePickerProps } from '../types'; -const DatePicker = (props) => { +const DatePicker: React.FC<BaseFieldProps<DatePickerProps>> = (props) => { const { label, isRequired, helperText, meta, validateOnMount, description, hideLabel, input, isReadOnly, isDisabled, id, FormGroupProps, ...rest } = useFieldApi(props); + return ( <FormGroup label={label} @@ -20,7 +22,7 @@ const DatePicker = (props) => { > <PF4DatePicker {...input} - onChange={(_e, value) => input.onChange(value)} + onChange={(_e: React.FormEvent<HTMLInputElement>, value: string) => input.onChange(value)} {...rest} id={id || input.name} isDisabled={isDisabled || isReadOnly} diff --git a/packages/pf4-component-mapper/src/date-picker/index.js b/packages/pf4-component-mapper/src/date-picker/index.js deleted file mode 100644 index 921dafa84..000000000 --- a/packages/pf4-component-mapper/src/date-picker/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './date-picker'; -export * from './date-picker'; diff --git a/packages/pf4-component-mapper/src/date-picker/index.d.ts b/packages/pf4-component-mapper/src/date-picker/index.tsx similarity index 56% rename from packages/pf4-component-mapper/src/date-picker/index.d.ts rename to packages/pf4-component-mapper/src/date-picker/index.tsx index 921dafa84..0b76f76fd 100644 --- a/packages/pf4-component-mapper/src/date-picker/index.d.ts +++ b/packages/pf4-component-mapper/src/date-picker/index.tsx @@ -1,2 +1 @@ export { default } from './date-picker'; -export * from './date-picker'; diff --git a/packages/pf4-component-mapper/src/dual-list-context/dual-list-context.d.ts b/packages/pf4-component-mapper/src/dual-list-context/dual-list-context.d.ts deleted file mode 100644 index e9cd3f845..000000000 --- a/packages/pf4-component-mapper/src/dual-list-context/dual-list-context.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface SortConfig { - left: 'asc' | 'desc'; - right: 'asc' | 'desc'; -} - -export type SetSortConfig = (newSortConfig: SortConfig) => void; - -export interface DualListContextValue { - sortConfig: SortConfig; - setSortConfig: SetSortConfig; -} - -export interface DualListContext { - value: DualListContextValue; -} - -export default DualListContext; diff --git a/packages/pf4-component-mapper/src/dual-list-context/dual-list-context.js b/packages/pf4-component-mapper/src/dual-list-context/dual-list-context.tsx similarity index 60% rename from packages/pf4-component-mapper/src/dual-list-context/dual-list-context.js rename to packages/pf4-component-mapper/src/dual-list-context/dual-list-context.tsx index 80e7f1d44..5b06adac6 100644 --- a/packages/pf4-component-mapper/src/dual-list-context/dual-list-context.js +++ b/packages/pf4-component-mapper/src/dual-list-context/dual-list-context.tsx @@ -1,5 +1,5 @@ import { createContext } from 'react'; -const DualListContext = createContext({}); +const DualListContext = createContext<any>({}); export default DualListContext; diff --git a/packages/pf4-component-mapper/src/dual-list-context/index.js b/packages/pf4-component-mapper/src/dual-list-context/index.js deleted file mode 100644 index db6eabec0..000000000 --- a/packages/pf4-component-mapper/src/dual-list-context/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './dual-list-context'; -export * from './dual-list-context'; diff --git a/packages/pf4-component-mapper/src/dual-list-context/index.d.ts b/packages/pf4-component-mapper/src/dual-list-context/index.tsx similarity index 55% rename from packages/pf4-component-mapper/src/dual-list-context/index.d.ts rename to packages/pf4-component-mapper/src/dual-list-context/index.tsx index db6eabec0..c92f7b855 100644 --- a/packages/pf4-component-mapper/src/dual-list-context/index.d.ts +++ b/packages/pf4-component-mapper/src/dual-list-context/index.tsx @@ -1,2 +1 @@ export { default } from './dual-list-context'; -export * from './dual-list-context'; diff --git a/packages/pf4-component-mapper/src/dual-list-select/dual-list-select.d.ts b/packages/pf4-component-mapper/src/dual-list-select/dual-list-select.d.ts deleted file mode 100644 index 31ad843fd..000000000 --- a/packages/pf4-component-mapper/src/dual-list-select/dual-list-select.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { UseFieldApiComponentConfig, AnyObject } from "@data-driven-forms/react-form-renderer"; -import { DualListSelectorProps } from "@patternfly/react-core"; -import { ReactNode } from "react"; -import FormGroupProps from "../form-group"; -export interface DualListSelectOption extends AnyObject { - value: any; - label: ReactNode | string; -} - -export type GetValueFromNode = (node: ReactNode) => string; - -interface InternalDualListSelectProps { - options: Array<DualListSelectOption | string>; - getValueFromNode?: GetValueFromNode; - isSortable?: boolean; -} - -export type DualListSelectProps = InternalDualListSelectProps & FormGroupProps & UseFieldApiComponentConfig & DualListSelectorProps; - -declare const DualListSelect: React.ComponentType<DualListSelectProps>; - -export default DualListSelect; diff --git a/packages/pf4-component-mapper/src/dual-list-select/dual-list-select.js b/packages/pf4-component-mapper/src/dual-list-select/dual-list-select.js deleted file mode 100644 index c4915b658..000000000 --- a/packages/pf4-component-mapper/src/dual-list-select/dual-list-select.js +++ /dev/null @@ -1,121 +0,0 @@ -import React, { useState } from 'react'; -import { DualListSelector } from '@patternfly/react-core/deprecated'; -import { useFieldApi } from '@data-driven-forms/react-form-renderer'; -import isEqual from 'lodash/isEqual'; - -import DualListTree from '../dual-list-tree-select/dual-list-tree-select'; - -import FormGroup from '../form-group/form-group'; -import DualListContext from '../dual-list-context/dual-list-context'; - -const DualList = (props) => { - const { - label, - isRequired, - helperText, - meta, - validateOnMount, - description, - hideLabel, - id, - input, - FormGroupProps, - options, - getValueFromNode, - isSearchable, - isSortable, - ...rest - } = useFieldApi({ - ...props, - FieldProps: { - isEqual: (current, initial) => isEqual([...(current || [])].sort(), [...(initial || [])].sort()), - }, - }); - - const [sortConfig, setSortConfig] = useState(() => ({ left: isSortable && 'asc', right: isSortable && 'asc' })); - - const value = input.value || []; - - let leftOptions; - let rightOptions; - let onListChange; - let filterOption; - - if (!getValueFromNode) { - leftOptions = options - .filter((option) => (typeof option === 'object' ? !value.includes(option.value) : !value.includes(option))) - .map((option) => option.label || option); - - rightOptions = options - .filter((option) => (typeof option === 'object' ? value.includes(option.value) : value.includes(option))) - .map((option) => option.label || option); - - onListChange = (_e, _newLeft, newRight) => { - input.onChange(newRight); - }; - - filterOption = (option, input) => (option.value ? option.value.includes(input) : option.includes(input)); - } else { - leftOptions = options - .filter((option) => (option.value ? !value.includes(option.value) : !value.includes(getValueFromNode(option)))) - .map((option) => option.label || option); - - rightOptions = options - .filter((option) => (option.value ? value.includes(option.value) : value.includes(getValueFromNode(option)))) - .map((option) => option.label || option); - - onListChange = (_e, _newLeft, newRight) => { - input.onChange(newRight?.map(getValueFromNode)); - }; - - filterOption = (option, input) => (option.value ? option.value.includes(input) : getValueFromNode(option).includes(input)); - } - - if (isSortable) { - const sort = (direction, a, b) => (direction === 'asc' ? a.localeCompare(b) : b.localeCompare(a)); - - if (!getValueFromNode) { - leftOptions = leftOptions.sort((a, b) => sort(sortConfig.left, a.label || a, b.label || b)); - rightOptions = rightOptions.sort((a, b) => sort(sortConfig.right, a.label || a, b.label || b)); - } else { - leftOptions = leftOptions.sort((a, b) => sort(sortConfig.left, getValueFromNode(a.label || a), getValueFromNode(b.label || b))); - rightOptions = rightOptions.sort((a, b) => sort(sortConfig.right, getValueFromNode(a.label || a), getValueFromNode(b.label || b))); - } - } - - return ( - <FormGroup - label={label} - isRequired={isRequired} - helperText={helperText} - meta={meta} - validateOnMount={validateOnMount} - description={description} - hideLabel={hideLabel} - id={id || input.name} - FormGroupProps={FormGroupProps} - > - <DualListContext.Provider value={{ sortConfig, setSortConfig }}> - <DualListSelector - availableOptions={leftOptions} - chosenOptions={rightOptions} - onListChange={onListChange} - id={id || input.name} - isSearchable={isSearchable} - {...(getValueFromNode && { - addAll: onListChange, - addSelected: onListChange, - filterOption, - removeAll: onListChange, - removeSelected: onListChange, - })} - {...rest} - /> - </DualListContext.Provider> - </FormGroup> - ); -}; - -const DualListWrapper = (props) => (props.isTree ? <DualListTree {...props} /> : <DualList {...props} />); - -export default DualListWrapper; diff --git a/packages/pf4-component-mapper/src/dual-list-select/dual-list-select.tsx b/packages/pf4-component-mapper/src/dual-list-select/dual-list-select.tsx new file mode 100644 index 000000000..59f0540b9 --- /dev/null +++ b/packages/pf4-component-mapper/src/dual-list-select/dual-list-select.tsx @@ -0,0 +1,119 @@ +import React, { useState } from 'react'; +import { DualListSelector, DualListSelectorProps } from '@patternfly/react-core/deprecated'; +import { useFieldApi } from '@data-driven-forms/react-form-renderer'; +import isEqual from 'lodash/isEqual'; + +import DualListTree from '../dual-list-tree-select/dual-list-tree-select'; + +import FormGroup from '../form-group/form-group'; +import DualListContext from '../dual-list-context/dual-list-context'; +import { BaseFieldProps, DualListSelectProps } from '../types'; + +const DualList: React.FC<BaseFieldProps<DualListSelectProps>> = (props) => { + const { + label, + isRequired, + helperText, + meta, + validateOnMount, + description, + hideLabel, + id, + input, + FormGroupProps, + options = [], + getValueFromNode, + isSearchable, + isSortable, + ...rest + } = useFieldApi({ + ...props, + FieldProps: { + isEqual: (current: any, initial: any) => isEqual([...(current || [])].sort(), [...(initial || [])].sort()), + }, + }); + + const [sortConfig, setSortConfig] = useState(() => ({ left: isSortable && 'asc', right: isSortable && 'asc' })); + + const value = input.value || []; + + let leftOptions: DualListSelectorProps['availableOptions']; + let rightOptions: DualListSelectorProps['chosenOptions']; + let onListChange: (event: any, newLeft: any[], newRight: any[]) => void; + let filterOption: (option: any, input: string) => boolean; + + if (!getValueFromNode) { + leftOptions = options + .filter((option: any) => (typeof option === 'object' ? !value.includes(option.value) : !value.includes(option))) + .map((option: any) => option.label || option); + + rightOptions = options + .filter((option: any) => (typeof option === 'object' ? value.includes(option.value) : value.includes(option))) + .map((option: any) => option.label || option); + + onListChange = (_e: any, _newLeft: any[], newRight: any[]) => { + input.onChange(newRight); + }; + + filterOption = (option: any, inputStr: string) => (option.value ? option.value.includes(inputStr) : option.includes(inputStr)); + } else { + leftOptions = options + .filter((option: any) => (option.value ? !value.includes(option.value) : !value.includes(getValueFromNode(option)))) + .map((option: any) => option.label || option); + + rightOptions = options + .filter((option: any) => (option.value ? value.includes(option.value) : value.includes(getValueFromNode(option)))) + .map((option: any) => option.label || option); + + onListChange = (_e: any, _newLeft: any[], newRight: any[]) => { + input.onChange(newRight?.map(getValueFromNode)); + }; + + filterOption = (option: any, inputStr: string) => (option.value ? option.value.includes(inputStr) : getValueFromNode(option).includes(inputStr)); + } + + if (isSortable) { + const sort = (direction: string, a: string, b: string) => (direction === 'asc' ? a.localeCompare(b) : b.localeCompare(a)); + + if (!getValueFromNode) { + leftOptions = leftOptions.sort((a: any, b: any) => sort(sortConfig.left, a.label || a, b.label || b)); + rightOptions = rightOptions.sort((a: any, b: any) => sort(sortConfig.right, a.label || a, b.label || b)); + } else { + leftOptions = leftOptions.sort((a: any, b: any) => sort(sortConfig.left, getValueFromNode(a.label || a), getValueFromNode(b.label || b))); + rightOptions = rightOptions.sort((a: any, b: any) => sort(sortConfig.right, getValueFromNode(a.label || a), getValueFromNode(b.label || b))); + } + } + + return ( + <FormGroup + label={label} + isRequired={isRequired} + helperText={helperText} + meta={meta} + validateOnMount={validateOnMount} + description={description} + hideLabel={hideLabel} + id={id || input.name} + FormGroupProps={FormGroupProps} + > + <DualListContext.Provider value={{ sortConfig, setSortConfig }}> + <DualListSelector + availableOptions={leftOptions} + chosenOptions={rightOptions} + onListChange={onListChange} + id={id || input.name} + isSearchable={isSearchable} + {...(getValueFromNode && { + filterOption, + })} + {...rest} + /> + </DualListContext.Provider> + </FormGroup> + ); +}; + +const DualListWrapper: React.FC<BaseFieldProps<DualListSelectProps> & { isTree?: boolean }> = (props) => + props.isTree ? <DualListTree {...props} /> : <DualList {...props} />; + +export default DualListWrapper; diff --git a/packages/pf4-component-mapper/src/dual-list-select/index.js b/packages/pf4-component-mapper/src/dual-list-select/index.js deleted file mode 100644 index 34150f77b..000000000 --- a/packages/pf4-component-mapper/src/dual-list-select/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './dual-list-select'; -export * from './dual-list-select'; diff --git a/packages/pf4-component-mapper/src/dual-list-select/index.d.ts b/packages/pf4-component-mapper/src/dual-list-select/index.tsx similarity index 56% rename from packages/pf4-component-mapper/src/dual-list-select/index.d.ts rename to packages/pf4-component-mapper/src/dual-list-select/index.tsx index 34150f77b..c0c7d5074 100644 --- a/packages/pf4-component-mapper/src/dual-list-select/index.d.ts +++ b/packages/pf4-component-mapper/src/dual-list-select/index.tsx @@ -1,2 +1 @@ export { default } from './dual-list-select'; -export * from './dual-list-select'; diff --git a/packages/pf4-component-mapper/src/dual-list-sort-button/dual-list-sort-button.d.ts b/packages/pf4-component-mapper/src/dual-list-sort-button/dual-list-sort-button.d.ts deleted file mode 100644 index e017c43fd..000000000 --- a/packages/pf4-component-mapper/src/dual-list-sort-button/dual-list-sort-button.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { AnyObject } from "@data-driven-forms/react-form-renderer"; - -export interface DualListSortButtonProps extends AnyObject { - position: 'left' | 'right'; -} - -declare const DualListSortButton: React.ComponentType<DualListSortButtonProps>; - -export default DualListSortButton; diff --git a/packages/pf4-component-mapper/src/dual-list-sort-button/dual-list-sort-button.js b/packages/pf4-component-mapper/src/dual-list-sort-button/dual-list-sort-button.tsx similarity index 82% rename from packages/pf4-component-mapper/src/dual-list-sort-button/dual-list-sort-button.js rename to packages/pf4-component-mapper/src/dual-list-sort-button/dual-list-sort-button.tsx index 6cc493706..cc16e631d 100644 --- a/packages/pf4-component-mapper/src/dual-list-sort-button/dual-list-sort-button.js +++ b/packages/pf4-component-mapper/src/dual-list-sort-button/dual-list-sort-button.tsx @@ -5,7 +5,12 @@ import { PficonSortCommonAscIcon, PficonSortCommonDescIcon } from '@patternfly/r import DualListContext from '../dual-list-context/dual-list-context'; -const DualListSortButton = ({ position, ...rest }) => { +interface DualListSortButtonProps { + position: string; + [key: string]: any; +} + +const DualListSortButton: React.FC<DualListSortButtonProps> = ({ position, ...rest }) => { const { sortConfig, setSortConfig } = useContext(DualListContext); return ( diff --git a/packages/pf4-component-mapper/src/dual-list-sort-button/index.js b/packages/pf4-component-mapper/src/dual-list-sort-button/index.js deleted file mode 100644 index a9c7d2225..000000000 --- a/packages/pf4-component-mapper/src/dual-list-sort-button/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './dual-list-sort-button'; -export * from './dual-list-sort-button'; diff --git a/packages/pf4-component-mapper/src/dual-list-sort-button/index.d.ts b/packages/pf4-component-mapper/src/dual-list-sort-button/index.tsx similarity index 55% rename from packages/pf4-component-mapper/src/dual-list-sort-button/index.d.ts rename to packages/pf4-component-mapper/src/dual-list-sort-button/index.tsx index a9c7d2225..698aa05ed 100644 --- a/packages/pf4-component-mapper/src/dual-list-sort-button/index.d.ts +++ b/packages/pf4-component-mapper/src/dual-list-sort-button/index.tsx @@ -1,2 +1 @@ export { default } from './dual-list-sort-button'; -export * from './dual-list-sort-button'; diff --git a/packages/pf4-component-mapper/src/dual-list-tree-select/dual-list-tree-select.d.ts b/packages/pf4-component-mapper/src/dual-list-tree-select/dual-list-tree-select.d.ts deleted file mode 100644 index 1f2812e72..000000000 --- a/packages/pf4-component-mapper/src/dual-list-tree-select/dual-list-tree-select.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { UseFieldApiComponentConfig } from "@data-driven-forms/react-form-renderer"; -import { DualListSelectorProps } from "@patternfly/react-core"; -import { DualListSelectorTreeItemProps } from "@patternfly/react-core/dist/js/components/DualListSelector/DualListSelectorTreeItem"; -import FormGroupProps from "../form-group"; - -export interface DualListTreeSelectOption extends DualListSelectorTreeItemProps { - value: any; -} - -interface InternalDualListSelectProps { - options: Array<DualListTreeSelectOption | string>; - isSortable?: boolean; -} - -export type DualListTreeSelectProps = InternalDualListSelectProps & FormGroupProps & UseFieldApiComponentConfig & DualListSelectorProps; - -declare const DualListSelectTree: React.ComponentType<DualListTreeSelectProps>; - -export default DualListSelectTree; diff --git a/packages/pf4-component-mapper/src/dual-list-tree-select/dual-list-tree-select.js b/packages/pf4-component-mapper/src/dual-list-tree-select/dual-list-tree-select.tsx similarity index 63% rename from packages/pf4-component-mapper/src/dual-list-tree-select/dual-list-tree-select.js rename to packages/pf4-component-mapper/src/dual-list-tree-select/dual-list-tree-select.tsx index 79c99aa28..e950b489b 100644 --- a/packages/pf4-component-mapper/src/dual-list-tree-select/dual-list-tree-select.js +++ b/packages/pf4-component-mapper/src/dual-list-tree-select/dual-list-tree-select.tsx @@ -5,15 +5,16 @@ import isEqual from 'lodash/isEqual'; import FormGroup from '../form-group/form-group'; import DualListContext from '../dual-list-context/dual-list-context'; +import { BaseFieldProps, DualListSelectProps } from '../types'; -export const convertOptions = (options, sort) => { +export const convertOptions = (options: any, sort?: string): any => { if (Array.isArray(options)) { - let result = options.map((option) => convertOptions(option, sort)).filter(Boolean); + let result = options.map((option: any) => convertOptions(option, sort)).filter(Boolean); if (sort) { - const sortFn = (a, b) => (sort === 'asc' ? a.localeCompare(b) : b.localeCompare(a)); + const sortFn = (a: any, b: any) => (sort === 'asc' ? a.localeCompare(b) : b.localeCompare(a)); - result = result.sort((a, b) => sortFn(a.text || a.label, b.text || b.label)); + result = result.sort((a: any, b: any) => sortFn(a.text || a.label, b.text || b.label)); } return result; @@ -28,9 +29,9 @@ export const convertOptions = (options, sort) => { }; }; -export const selectedOptions = (options, value, selected) => { +export const selectedOptions = (options: any, value: any[], selected: boolean): any => { if (Array.isArray(options)) { - return options.map((option) => selectedOptions(option, value, selected)).filter(Boolean); + return options.map((option: any) => selectedOptions(option, value, selected)).filter(Boolean); } if (options.value) { @@ -51,9 +52,9 @@ export const selectedOptions = (options, value, selected) => { } }; -export const getValueFromSelected = (options, newValue = []) => { +export const getValueFromSelected = (options: any, newValue: any[] = []): any[] => { if (Array.isArray(options)) { - options.map((option) => getValueFromSelected(option, newValue)); + options.map((option: any) => getValueFromSelected(option, newValue)); } if (options.value) { @@ -67,14 +68,27 @@ export const getValueFromSelected = (options, newValue = []) => { return newValue; }; -const DualListTreeSelect = (props) => { - const { label, isRequired, helperText, meta, validateOnMount, description, hideLabel, id, input, FormGroupProps, options, isSortable, ...rest } = - useFieldApi({ - ...props, - FieldProps: { - isEqual: (current, initial) => isEqual([...(current || [])].sort(), [...(initial || [])].sort()), - }, - }); +const DualListTreeSelect: React.FC<BaseFieldProps<DualListSelectProps>> = (props) => { + const { + label, + isRequired, + helperText, + meta, + validateOnMount, + description, + hideLabel, + id, + input, + FormGroupProps, + options = [], + isSortable, + ...rest + } = useFieldApi({ + ...props, + FieldProps: { + isEqual: (current: any, initial: any) => isEqual([...(current || [])].sort(), [...(initial || [])].sort()), + }, + }); const [sortConfig, setSortConfig] = useState(() => ({ left: isSortable && 'asc', right: isSortable && 'asc' })); @@ -83,7 +97,7 @@ const DualListTreeSelect = (props) => { const leftOptions = selectedOptions(options, value, false); const rightOptions = selectedOptions(options, value, true); - const onListChange = (_e, _newLeft, newRight) => input.onChange(getValueFromSelected(newRight)); + const onListChange = (_e: any, _newLeft: any, newRight: any) => input.onChange(getValueFromSelected(newRight)); return ( <FormGroup diff --git a/packages/pf4-component-mapper/src/dual-list-tree-select/index.d.ts b/packages/pf4-component-mapper/src/dual-list-tree-select/index.d.ts deleted file mode 100644 index 811572091..000000000 --- a/packages/pf4-component-mapper/src/dual-list-tree-select/index.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './dual-list-tree-select'; -export * from './dual-list-tree-select'; diff --git a/packages/pf4-component-mapper/src/dual-list-tree-select/index.js b/packages/pf4-component-mapper/src/dual-list-tree-select/index.js deleted file mode 100644 index 811572091..000000000 --- a/packages/pf4-component-mapper/src/dual-list-tree-select/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './dual-list-tree-select'; -export * from './dual-list-tree-select'; diff --git a/packages/pf4-component-mapper/src/dual-list-tree-select/index.tsx b/packages/pf4-component-mapper/src/dual-list-tree-select/index.tsx new file mode 100644 index 000000000..172900154 --- /dev/null +++ b/packages/pf4-component-mapper/src/dual-list-tree-select/index.tsx @@ -0,0 +1 @@ +export { default, convertOptions, selectedOptions, getValueFromSelected } from './dual-list-tree-select'; diff --git a/packages/pf4-component-mapper/src/field-array/field-array.d.ts b/packages/pf4-component-mapper/src/field-array/field-array.d.ts deleted file mode 100644 index ba87b0344..000000000 --- a/packages/pf4-component-mapper/src/field-array/field-array.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ReactNode } from "react"; -import { FieldArrayField } from "@data-driven-forms/react-form-renderer"; - -interface FieldArrayButtonLabels { - add?: ReactNode; - remove?: ReactNode; - removeAll?: ReactNode; -} - -export interface FieldArrayProps { - label?: ReactNode; - description?: ReactNode; - fields: FieldArrayField[]; - defaultItem?: any; - minItems?: number; - maxItems?: number; - noItemsMessage?: ReactNode; - name: string; - buttonLabels?: FieldArrayButtonLabels; -} - -declare const FieldArray: React.ComponentType<FieldArrayProps>; - -export default FieldArray; diff --git a/packages/pf4-component-mapper/src/field-array/field-array.js b/packages/pf4-component-mapper/src/field-array/field-array.tsx similarity index 73% rename from packages/pf4-component-mapper/src/field-array/field-array.js rename to packages/pf4-component-mapper/src/field-array/field-array.tsx index 25838fac5..f11a4ed4a 100644 --- a/packages/pf4-component-mapper/src/field-array/field-array.js +++ b/packages/pf4-component-mapper/src/field-array/field-array.tsx @@ -1,21 +1,32 @@ import React, { memo } from 'react'; import isEqual from 'lodash/isEqual'; -import { useFormApi, FieldArray } from '@data-driven-forms/react-form-renderer'; +import { useFormApi, FieldArray, useFieldApi } from '@data-driven-forms/react-form-renderer'; import { Bullseye, Button, Flex, FlexItem, FormFieldGroup, FormFieldGroupHeader, FormHelperText, Grid, GridItem } from '@patternfly/react-core'; import { TrashIcon } from '@patternfly/react-icons'; import './final-form-array.css'; -import { useFieldApi } from '@data-driven-forms/react-form-renderer'; +import { BaseFieldProps, FieldArrayProps, FieldArrayButtonLabels } from '../types'; -const Spacer = () => <span className="ddf-final-form-spacer" />; +const Spacer: React.FC = () => <span className="ddf-final-form-spacer" />; -const ArrayItem = memo( +interface ArrayItemProps { + fields: any[]; + fieldIndex: number; + name: string; + remove: (index: number) => void; + length: number; + minItems: number; + buttonLabels: Required<FieldArrayButtonLabels>; + isLast: boolean; +} + +const ArrayItem = memo<ArrayItemProps>( ({ fields, fieldIndex, name, remove, length, minItems, buttonLabels, isLast }) => { const { renderForm } = useFormApi(); - const editedFields = fields.map((field, index) => { + const editedFields = fields.map((field: any, index: number) => { const computedName = field.name ? `${name}.${field.name}` : name; return { ...field, name: computedName, key: `${name}-${index}` }; }); @@ -25,8 +36,8 @@ const ArrayItem = memo( return ( <React.Fragment> <Flex> - <FlexItem className="pf-c-form" grow={{ default: 'flex_1' }}> - {editedFields.map((field) => renderForm([field]))} + <FlexItem className="pf-c-form" grow={{ default: 'grow' }}> + {editedFields.map((field: any) => renderForm([field]))} </FlexItem> <FlexItem> {editedFields[0].label && <Spacer />} @@ -46,24 +57,27 @@ const ArrayItem = memo( ({ remove: _prevRemove, ...prev }, { remove: _nextRemove, ...next }) => isEqual(prev, next) ); -const DynamicArray = ({ ...props }) => { +ArrayItem.displayName = 'ArrayItem'; + +const DynamicArray: React.FC<BaseFieldProps<FieldArrayProps>> = (props) => { const { arrayValidator, label, description, - fields: formFields, + fields: formFields = [], defaultItem, meta, minItems = 0, maxItems = Infinity, noItemsMessage = 'No items added', - buttonLabels, + buttonLabels = {}, ...rest } = useFieldApi(props); + const { dirty, submitFailed, error, submitError } = meta; const isError = (dirty || submitFailed) && (error || submitError) && (typeof error === 'string' || typeof submitError === 'string'); - const combinedButtonLabels = { + const combinedButtonLabels: Required<FieldArrayButtonLabels> = { add: 'Add item', removeAll: 'Delete all', remove: 'Remove', @@ -72,7 +86,7 @@ const DynamicArray = ({ ...props }) => { return ( <FieldArray key={rest.input.name} name={rest.input.name} validate={arrayValidator}> - {({ fields: { map, value = [], push, remove, removeBatch } }) => ( + {({ fields: { map, value = [], push, remove, forEach } }) => ( <FormFieldGroup header={ <FormFieldGroupHeader @@ -84,7 +98,7 @@ const DynamicArray = ({ ...props }) => { <Button variant="link" isDisabled={value.length === 0} - {...(value.length !== 0 && { onClick: () => removeBatch(value.map((_, index) => index)) })} + {...(value.length !== 0 && { onClick: () => forEach((_: any, index: number) => remove(index)) })} > {combinedButtonLabels.removeAll} </Button> @@ -102,7 +116,7 @@ const DynamicArray = ({ ...props }) => { } > {value.length <= 0 && <Bullseye>{noItemsMessage}</Bullseye>} - {map((name, index) => ( + {map((name: string, index: number) => ( <ArrayItem key={`${name}-${index}`} fields={formFields} @@ -116,13 +130,7 @@ const DynamicArray = ({ ...props }) => { /> ))} <Grid> - <GridItem sm={11}> - {isError && ( - <FormHelperText isHidden={false} isError={true}> - {error || submitError} - </FormHelperText> - )} - </GridItem> + <GridItem sm={11}>{isError && <FormHelperText>{error || submitError}</FormHelperText>}</GridItem> </Grid> </FormFieldGroup> )} diff --git a/packages/pf4-component-mapper/src/field-array/index.js b/packages/pf4-component-mapper/src/field-array/index.js deleted file mode 100644 index 2dbda3dc9..000000000 --- a/packages/pf4-component-mapper/src/field-array/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './field-array'; -export * from './field-array'; diff --git a/packages/pf4-component-mapper/src/field-array/index.d.ts b/packages/pf4-component-mapper/src/field-array/index.tsx similarity index 100% rename from packages/pf4-component-mapper/src/field-array/index.d.ts rename to packages/pf4-component-mapper/src/field-array/index.tsx diff --git a/packages/pf4-component-mapper/src/form-group/form-group.d.ts b/packages/pf4-component-mapper/src/form-group/form-group.d.ts deleted file mode 100644 index 3dd0250a9..000000000 --- a/packages/pf4-component-mapper/src/form-group/form-group.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ReactNode } from 'react'; -import { FormGroupProps as PfFormGroupProps } from '@patternfly/react-core'; - -interface FormGroupProps { - description?: ReactNode; - hideLabel?: boolean; - id?: string; - label?: ReactNode; - isRequired?: boolean; - helperText?: ReactNode; - FormGroupProps?: PfFormGroupProps; -} - -export default FormGroupProps; diff --git a/packages/pf4-component-mapper/src/form-group/form-group.js b/packages/pf4-component-mapper/src/form-group/form-group.tsx similarity index 66% rename from packages/pf4-component-mapper/src/form-group/form-group.js rename to packages/pf4-component-mapper/src/form-group/form-group.tsx index 5eacc57dc..9f31eaace 100644 --- a/packages/pf4-component-mapper/src/form-group/form-group.js +++ b/packages/pf4-component-mapper/src/form-group/form-group.tsx @@ -2,12 +2,31 @@ import React from 'react'; import { FormGroup as Pf4FormGroup, Content, FormHelperText, HelperText, HelperTextItem } from '@patternfly/react-core'; import showError from '../show-error/show-error'; +import { FormGroupComponentProps } from '../types'; -const FormGroup = ({ label, isRequired, helperText, meta, validateOnMount, description, hideLabel, children, id, FormGroupProps }) => { +const FormGroup: React.FC<FormGroupComponentProps> = ({ + label, + isRequired, + helperText, + meta, + validateOnMount, + description, + hideLabel, + children, + id, + FormGroupProps, +}) => { const { validated } = showError(meta, validateOnMount); const validationInternal = (meta.touched || validateOnMount) && (meta.error || meta.submitError || meta.warning); + return ( - <Pf4FormGroup isRequired={isRequired} label={!hideLabel && label} fieldId={id} aria-label={meta.error || meta.submitError} {...FormGroupProps}> + <Pf4FormGroup + isRequired={isRequired} + label={!hideLabel ? label : undefined} + fieldId={id} + aria-label={meta.error || meta.submitError} + {...FormGroupProps} + > {description && ( <Content> <Content component="small">{description}</Content> diff --git a/packages/pf4-component-mapper/src/form-group/index.js b/packages/pf4-component-mapper/src/form-group/index.js deleted file mode 100644 index b03633387..000000000 --- a/packages/pf4-component-mapper/src/form-group/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './form-group'; -export * from './form-group'; diff --git a/packages/pf4-component-mapper/src/form-group/index.d.ts b/packages/pf4-component-mapper/src/form-group/index.ts similarity index 100% rename from packages/pf4-component-mapper/src/form-group/index.d.ts rename to packages/pf4-component-mapper/src/form-group/index.ts diff --git a/packages/pf4-component-mapper/src/form-template/form-template.d.ts b/packages/pf4-component-mapper/src/form-template/form-template.d.ts deleted file mode 100644 index 79708949f..000000000 --- a/packages/pf4-component-mapper/src/form-template/form-template.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -import FormTemplateCommonProps from "@data-driven-forms/common/form-template"; - -declare const FormTemplate: React.ComponentType<FormTemplateCommonProps>; - -export default FormTemplate; diff --git a/packages/pf4-component-mapper/src/form-template/form-template.js b/packages/pf4-component-mapper/src/form-template/form-template.tsx similarity index 58% rename from packages/pf4-component-mapper/src/form-template/form-template.js rename to packages/pf4-component-mapper/src/form-template/form-template.tsx index 28750ad79..10c677a18 100644 --- a/packages/pf4-component-mapper/src/form-template/form-template.js +++ b/packages/pf4-component-mapper/src/form-template/form-template.tsx @@ -3,17 +3,24 @@ import React from 'react'; import FormTemplate from '@data-driven-forms/common/form-template'; import { Button as PF4Button, ActionGroup, Form, Content, ContentVariants, Alert } from '@patternfly/react-core'; +import { + FormTemplateButtonProps, + FormTemplateButtonGroupProps, + FormTemplateTitleProps, + FormTemplateDescriptionProps, + FormTemplateFormErrorProps, +} from '../types'; -export const Button = ({ label, bsStyle, children, disabled, buttonType, ...props }) => ( +export const Button: React.FC<FormTemplateButtonProps> = ({ label, bsStyle, children, disabled, buttonType, ...props }) => ( <PF4Button variant={buttonType === 'cancel' ? 'link' : bsStyle || 'secondary'} isDisabled={disabled} {...props}> {label} {children} </PF4Button> ); -export const ButtonGroup = ({ children, ...props }) => <ActionGroup {...props}>{children}</ActionGroup>; +export const ButtonGroup: React.FC<FormTemplateButtonGroupProps> = ({ children, ...props }) => <ActionGroup {...props}>{children}</ActionGroup>; -export const Title = ({ children, ...props }) => ( +export const Title: React.FC<FormTemplateTitleProps> = ({ children, ...props }) => ( <Content> <Content component={ContentVariants.h1} {...props}> {children} @@ -21,7 +28,7 @@ export const Title = ({ children, ...props }) => ( </Content> ); -export const Description = ({ children, ...props }) => ( +export const Description: React.FC<FormTemplateDescriptionProps> = ({ children, ...props }) => ( <Content> <Content component={ContentVariants.p} {...props}> {children} @@ -29,8 +36,8 @@ export const Description = ({ children, ...props }) => ( </Content> ); -export const FormError = ({ formError, alertProps }) => { - if (typeof formError === 'object' && formError.title) { +export const FormError: React.FC<FormTemplateFormErrorProps> = ({ formError, alertProps }) => { + if (typeof formError === 'object' && formError && formError.title) { const { title, description, ...props } = formError; return ( @@ -47,7 +54,7 @@ export const FormError = ({ formError, alertProps }) => { return null; }; -const PF4FormTemplate = (props) => ( +const PF4FormTemplate: React.FC<any> = (props) => ( <FormTemplate BeforeError={FormError} FormWrapper={Form} diff --git a/packages/pf4-component-mapper/src/form-template/index.d.ts b/packages/pf4-component-mapper/src/form-template/index.d.ts deleted file mode 100644 index 4e2a6648b..000000000 --- a/packages/pf4-component-mapper/src/form-template/index.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './form-template'; -export * from './form-template'; diff --git a/packages/pf4-component-mapper/src/form-template/index.js b/packages/pf4-component-mapper/src/form-template/index.js deleted file mode 100644 index 4e2a6648b..000000000 --- a/packages/pf4-component-mapper/src/form-template/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './form-template'; -export * from './form-template'; diff --git a/packages/pf4-component-mapper/src/form-template/index.tsx b/packages/pf4-component-mapper/src/form-template/index.tsx new file mode 100644 index 000000000..ea711c304 --- /dev/null +++ b/packages/pf4-component-mapper/src/form-template/index.tsx @@ -0,0 +1 @@ +export { default, Button, ButtonGroup, Title, Description, FormError } from './form-template'; diff --git a/packages/pf4-component-mapper/src/index.js b/packages/pf4-component-mapper/src/index.ts similarity index 84% rename from packages/pf4-component-mapper/src/index.js rename to packages/pf4-component-mapper/src/index.ts index 222762a77..6256869a6 100644 --- a/packages/pf4-component-mapper/src/index.js +++ b/packages/pf4-component-mapper/src/index.ts @@ -15,6 +15,12 @@ export { default as SubForm } from './sub-form'; export { default as Switch } from './switch'; export { default as Tabs } from './tabs'; export { default as Textarea } from './textarea'; +export { default as TextField } from './text-field'; export { default as TimePicker } from './time-picker'; export { default as Wizard } from './wizard'; export { default as showError } from './show-error'; +export { default as FormGroup } from './form-group'; +export { default as IsRequired } from './is-required'; + +// Export types +export type * from './types'; \ No newline at end of file diff --git a/packages/pf4-component-mapper/src/is-required/index.js b/packages/pf4-component-mapper/src/is-required/index.js deleted file mode 100644 index 9c05a19e4..000000000 --- a/packages/pf4-component-mapper/src/is-required/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './is-required'; -export * from './is-required'; diff --git a/packages/pf4-component-mapper/src/is-required/index.d.ts b/packages/pf4-component-mapper/src/is-required/index.ts similarity index 100% rename from packages/pf4-component-mapper/src/is-required/index.d.ts rename to packages/pf4-component-mapper/src/is-required/index.ts diff --git a/packages/pf4-component-mapper/src/is-required/is-required.d.ts b/packages/pf4-component-mapper/src/is-required/is-required.d.ts deleted file mode 100644 index 3ad067201..000000000 --- a/packages/pf4-component-mapper/src/is-required/is-required.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -declare const IsRequired: React.ComponentType; - -export default IsRequired; diff --git a/packages/pf4-component-mapper/src/is-required/is-required.js b/packages/pf4-component-mapper/src/is-required/is-required.tsx similarity index 61% rename from packages/pf4-component-mapper/src/is-required/is-required.js rename to packages/pf4-component-mapper/src/is-required/is-required.tsx index e00ffa498..fd79a7dfd 100644 --- a/packages/pf4-component-mapper/src/is-required/is-required.js +++ b/packages/pf4-component-mapper/src/is-required/is-required.tsx @@ -1,6 +1,10 @@ import React from 'react'; -const IsRequired = ({ children }) => ( +interface IsRequiredProps { + children: React.ReactNode; +} + +const IsRequired: React.FC<IsRequiredProps> = ({ children }) => ( <React.Fragment> {children} <span className="pf-c-form__label-required" aria-hidden="true"> diff --git a/packages/pf4-component-mapper/src/plain-text/index.js b/packages/pf4-component-mapper/src/plain-text/index.js deleted file mode 100644 index f4ac5b13c..000000000 --- a/packages/pf4-component-mapper/src/plain-text/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './plain-text'; -export * from './plain-text'; diff --git a/packages/pf4-component-mapper/src/plain-text/index.d.ts b/packages/pf4-component-mapper/src/plain-text/index.ts similarity index 100% rename from packages/pf4-component-mapper/src/plain-text/index.d.ts rename to packages/pf4-component-mapper/src/plain-text/index.ts diff --git a/packages/pf4-component-mapper/src/plain-text/plain-text.d.ts b/packages/pf4-component-mapper/src/plain-text/plain-text.d.ts deleted file mode 100644 index 2a88cd681..000000000 --- a/packages/pf4-component-mapper/src/plain-text/plain-text.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ReactNode } from "react"; -import { ContentProps, TextContentProps } from "@patternfly/react-core"; - -interface InternalPlainTextProps { - label: ReactNode; - name: string; - variant?: 'p'|'span'|'strong'|'b'|'cite'|'caption'|'code'|'em'|'i'|'h1'|'h2'|'h3'|'h4'|'h5'|'h6'|'h6'|'div'|'label'|'pre'|'q'|'samp'|'small'|'sub'|'sup'; - TextContentProps: TextContentProps, -} - -export type PlainTextProps = InternalPlainTextProps & ContentProps; - -declare const PlainText: React.ComponentType<PlainTextProps>; - -export default PlainText; diff --git a/packages/pf4-component-mapper/src/plain-text/plain-text.js b/packages/pf4-component-mapper/src/plain-text/plain-text.js deleted file mode 100644 index ca47811e4..000000000 --- a/packages/pf4-component-mapper/src/plain-text/plain-text.js +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; - -import { Content } from '@patternfly/react-core'; - -const PlainText = ({ component, label, name, variant = 'p', TextContentProps, ...rest }) => ( - <Content {...TextContentProps}> - {typeof label === 'string' - ? label.split('\n').map((paragraph, index) => ( - <Content component={variant} {...rest} key={`${name}-${index}`}> - {paragraph} - </Content> - )) - : label} - </Content> -); - -export default PlainText; diff --git a/packages/pf4-component-mapper/src/plain-text/plain-text.tsx b/packages/pf4-component-mapper/src/plain-text/plain-text.tsx new file mode 100644 index 000000000..962506803 --- /dev/null +++ b/packages/pf4-component-mapper/src/plain-text/plain-text.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { Content, ContentProps } from '@patternfly/react-core'; + +export interface PlainTextProps { + component?: any; + label?: React.ReactNode; + name?: string; + variant?: ContentProps['component']; + TextContentProps?: ContentProps; + [key: string]: any; +} + +const PlainText: React.FC<PlainTextProps> = ({ component, label, name, variant = 'p', TextContentProps, ...rest }) => ( + <Content {...TextContentProps}> + {typeof label === 'string' + ? label.split('\n').map((paragraph, index) => ( + <Content component={variant} {...rest} key={`${name}-${index}`}> + {paragraph} + </Content> + )) + : label} + </Content> +); + +export default PlainText; + +// PlainTextProps is exported above diff --git a/packages/pf4-component-mapper/src/radio/index.js b/packages/pf4-component-mapper/src/radio/index.js deleted file mode 100644 index b42f424d1..000000000 --- a/packages/pf4-component-mapper/src/radio/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './radio'; -export * from './radio'; diff --git a/packages/pf4-component-mapper/src/radio/index.d.ts b/packages/pf4-component-mapper/src/radio/index.tsx similarity index 58% rename from packages/pf4-component-mapper/src/radio/index.d.ts rename to packages/pf4-component-mapper/src/radio/index.tsx index b42f424d1..02c6c40c8 100644 --- a/packages/pf4-component-mapper/src/radio/index.d.ts +++ b/packages/pf4-component-mapper/src/radio/index.tsx @@ -1,2 +1 @@ export { default } from './radio'; -export * from './radio'; diff --git a/packages/pf4-component-mapper/src/radio/radio.d.ts b/packages/pf4-component-mapper/src/radio/radio.d.ts deleted file mode 100644 index 68be94610..000000000 --- a/packages/pf4-component-mapper/src/radio/radio.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { UseFieldApiComponentConfig, AnyObject } from "@data-driven-forms/react-form-renderer"; -import FormGroupProps from "../form-group"; -import { ReactNode } from "react"; - -export interface RadioOption extends AnyObject { - label: ReactNode; - value?: any; -} - -interface InternalRadioProps { - name: string; - options: RadioOption[]; - isReadOnly?: boolean; - isDisabled?: boolean; -} - -export type RadioProps = InternalRadioProps & FormGroupProps & UseFieldApiComponentConfig; - -declare const Radio: React.ComponentType<RadioProps>; - -export default Radio; diff --git a/packages/pf4-component-mapper/src/radio/radio.js b/packages/pf4-component-mapper/src/radio/radio.tsx similarity index 74% rename from packages/pf4-component-mapper/src/radio/radio.js rename to packages/pf4-component-mapper/src/radio/radio.tsx index 8a6c0a219..e401227d3 100644 --- a/packages/pf4-component-mapper/src/radio/radio.js +++ b/packages/pf4-component-mapper/src/radio/radio.tsx @@ -2,8 +2,16 @@ import React from 'react'; import { useFieldApi } from '@data-driven-forms/react-form-renderer'; import { Radio as Pf4Radio } from '@patternfly/react-core'; import FormGroup from '../form-group/form-group'; +import { BaseFieldProps, RadioProps, SelectOption } from '../types'; -const RadioOption = ({ name, option: { value, label, ...restOption }, isDisabled, isReadOnly }) => { +interface RadioOptionProps { + name: string; + option: SelectOption; + isDisabled?: boolean; + isReadOnly?: boolean; +} + +const RadioOption: React.FC<RadioOptionProps> = ({ name, option: { value, label, ...restOption }, isDisabled, isReadOnly }) => { const { input } = useFieldApi({ name, value }); return ( <Pf4Radio @@ -14,14 +22,14 @@ const RadioOption = ({ name, option: { value, label, ...restOption }, isDisabled onChange={() => input.onChange(value)} label={label} id={`${name}-${value}`} - aria-label={label} + aria-label={typeof label === 'string' ? label : undefined} isDisabled={isDisabled || isReadOnly} {...restOption} /> ); }; -const Radio = ({ name, type, ...props }) => { +const Radio: React.FC<BaseFieldProps<RadioProps>> = ({ name, type, ...props }) => { /** * You cannot assign type radio to PF4 radio buttons input. It will break and will not set input value, only checked property * It has to be reqular input and we have change the radio value manully to the option value @@ -31,6 +39,7 @@ const Radio = ({ name, type, ...props }) => { name, ...props, }); + return ( <FormGroup label={label} @@ -43,7 +52,7 @@ const Radio = ({ name, type, ...props }) => { id={id || input.name} FormGroupProps={FormGroupProps} > - {options.map((option) => ( + {options.map((option: SelectOption) => ( <RadioOption key={option.value} name={name} option={option} isReadOnly={isReadOnly} isDisabled={isDisabled} /> ))} </FormGroup> diff --git a/packages/pf4-component-mapper/src/select/index.js b/packages/pf4-component-mapper/src/select/index.js deleted file mode 100644 index 27caaa296..000000000 --- a/packages/pf4-component-mapper/src/select/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './select'; -export * from './select'; diff --git a/packages/pf4-component-mapper/src/select/index.d.ts b/packages/pf4-component-mapper/src/select/index.ts similarity index 100% rename from packages/pf4-component-mapper/src/select/index.d.ts rename to packages/pf4-component-mapper/src/select/index.ts diff --git a/packages/pf4-component-mapper/src/select/select.d.ts b/packages/pf4-component-mapper/src/select/select.d.ts deleted file mode 100644 index 62e06df67..000000000 --- a/packages/pf4-component-mapper/src/select/select.d.ts +++ /dev/null @@ -1,45 +0,0 @@ -import FormGroupProps from "../form-group"; -import { UseFieldApiComponentConfig } from "@data-driven-forms/react-form-renderer"; -import { ReactNode } from "react"; - -export interface SelectOption { - label: ReactNode; - value?: any; - key?: string; -} - -interface BaseSelectProps { - options?: SelectOption[]; - selectVariant?: 'default' | 'createable'; - isSearchable?: boolean; - isDisabled?: boolean; - isClearable?: boolean; - name?: string; - showMoreLabel?: ReactNode; - showLessLabel?: ReactNode; - simpleValue?: boolean; - isMulti?: boolean; - loadOptions?: (inputValue?: string) => Promise<SelectOption[] | undefined>; - loadingMessage?: ReactNode; - updatingMessage?: ReactNode; - noOptionsMessage?: ReactNode; - menuIsPortal?: boolean; - placeholder?: ReactNode; - validated?: 'success' | 'error' | 'default'; - id?: string; -} - -export interface InternalSelectProps extends BaseSelectProps { - value?: any; - onChange?: (option?: any) => void; - invalid?: boolean; - pluckSingleValue?: boolean; -} - -export const InternalSelect: React.ComponentType<InternalSelectProps>; - -export type SelectProps = BaseSelectProps & FormGroupProps & UseFieldApiComponentConfig; - -declare const Select: React.ComponentType<SelectProps>; - -export default Select; diff --git a/packages/pf4-component-mapper/src/select/select.js b/packages/pf4-component-mapper/src/select/select.tsx similarity index 55% rename from packages/pf4-component-mapper/src/select/select.js rename to packages/pf4-component-mapper/src/select/select.tsx index b0d9fee5b..613d38575 100644 --- a/packages/pf4-component-mapper/src/select/select.js +++ b/packages/pf4-component-mapper/src/select/select.tsx @@ -2,10 +2,33 @@ import React from 'react'; import { useFieldApi } from '@data-driven-forms/react-form-renderer'; import FormGroup from '../form-group/form-group'; import DataDrivenSelect from './select/select'; +import { BaseFieldProps, SelectOption } from '../types'; -const Select = (props) => { +export interface SelectFieldProps extends BaseFieldProps { + options?: SelectOption[]; + placeholder?: string; + isSearchable?: boolean; + isMulti?: boolean; + isClearable?: boolean; + isFetching?: boolean; + noResultsMessage?: string; + noOptionsMessage?: string; + loadingMessage?: string; + updatingMessage?: string; + showMoreLabel?: string; + showLessLabel?: string; + simpleValue?: boolean; + menuIsPortal?: boolean; + menuPortalTarget?: Element; + onInputChange?: (value: string) => void; + originalOptions?: SelectOption[]; + [key: string]: any; +} + +const Select: React.FC<SelectFieldProps> = (props) => { const { label, isRequired, helperText, meta, validateOnMount, description, hideLabel, input, isReadOnly, isDisabled, id, FormGroupProps, ...rest } = useFieldApi(props); + return ( <FormGroup label={label} diff --git a/packages/pf4-component-mapper/src/select/select/clear-indicator.js b/packages/pf4-component-mapper/src/select/select/clear-indicator.tsx similarity index 73% rename from packages/pf4-component-mapper/src/select/select/clear-indicator.js rename to packages/pf4-component-mapper/src/select/select/clear-indicator.tsx index dfb9cf12f..4d7b515b3 100644 --- a/packages/pf4-component-mapper/src/select/select/clear-indicator.js +++ b/packages/pf4-component-mapper/src/select/select/clear-indicator.tsx @@ -1,9 +1,12 @@ import React from 'react'; - import { TimesCircleIcon } from '@patternfly/react-icons'; import './clear-indicator.css'; -const ClearIndicator = ({ clearSelection }) => ( +interface ClearIndicatorProps { + clearSelection: () => void; +} + +const ClearIndicator: React.FC<ClearIndicatorProps> = ({ clearSelection }) => ( <button onClick={(event) => { clearSelection(); diff --git a/packages/pf4-component-mapper/src/select/select/empty-options.js b/packages/pf4-component-mapper/src/select/select/empty-options.tsx similarity index 100% rename from packages/pf4-component-mapper/src/select/select/empty-options.js rename to packages/pf4-component-mapper/src/select/select/empty-options.tsx diff --git a/packages/pf4-component-mapper/src/select/select/input.js b/packages/pf4-component-mapper/src/select/select/input.tsx similarity index 68% rename from packages/pf4-component-mapper/src/select/select/input.js rename to packages/pf4-component-mapper/src/select/select/input.tsx index fa9518663..fbdec4796 100644 --- a/packages/pf4-component-mapper/src/select/select/input.js +++ b/packages/pf4-component-mapper/src/select/select/input.tsx @@ -1,9 +1,8 @@ import React from 'react'; import '@patternfly/react-styles/css/components/TextInputGroup/text-input-group.css'; - import './input.css'; -const getInputString = (filter, value) => { +const getInputString = (filter: any, value: any): string => { if (typeof filter === 'string') { return filter; } @@ -19,9 +18,20 @@ const getInputString = (filter, value) => { return ''; }; -const Input = ({ inputRef, isSearchable, isDisabled, getInputProps, value, ...props }) => { +interface InputProps { + inputRef: React.RefObject<HTMLInputElement>; + isSearchable?: boolean; + isDisabled?: boolean; + getInputProps: (props?: any) => any; + value?: any; + placeholder?: string; + [key: string]: any; +} + +const Input: React.FC<InputProps> = ({ inputRef, isSearchable, isDisabled, getInputProps, value, ...props }) => { const inputProps = getInputProps({ disabled: isDisabled }); const initialInputValue = getInputString(inputProps.value, value); + return ( <div className="ddorg__pf4-component-mapper__select-input-wrapper pf-v6-c-text-input-group pf-m-typeahead pf-m-plain"> <div className="pf-v6-c-text-input-group__main"> @@ -34,11 +44,11 @@ const Input = ({ inputRef, isSearchable, isDisabled, getInputProps, value, ...pr {...{ ...inputProps, value: initialInputValue, - onKeyDown: (event, ...args) => { + onKeyDown: (event: React.KeyboardEvent, ...args: any[]) => { event.stopPropagation(); inputProps.onKeyDown(event, ...args); }, - onChange: inputProps.onChange || Function, + onChange: inputProps.onChange || (() => {}), }} /> </span> diff --git a/packages/pf4-component-mapper/src/select/select/menu.js b/packages/pf4-component-mapper/src/select/select/menu.tsx similarity index 60% rename from packages/pf4-component-mapper/src/select/select/menu.js rename to packages/pf4-component-mapper/src/select/select/menu.tsx index 4b532a5d7..c83c496bd 100644 --- a/packages/pf4-component-mapper/src/select/select/menu.js +++ b/packages/pf4-component-mapper/src/select/select/menu.tsx @@ -3,10 +3,11 @@ import React, { useEffect, useState, useRef } from 'react'; import { createPortal } from 'react-dom'; import Option from './option'; import EmptyOption from './empty-options'; +import { SelectOption } from '../../types'; import './menu.css'; -const getScrollParent = (element) => { +const getScrollParent = (element: Element): Element => { let style = getComputedStyle(element); const excludeStaticParent = style.position === 'absolute'; const overflowRx = /(auto|scroll)/; @@ -30,7 +31,7 @@ const getScrollParent = (element) => { return docEl; }; -const getMenuPosition = (selectBase) => { +const getMenuPosition = (selectBase: Element | null) => { if (!selectBase) { return {}; } @@ -38,7 +39,7 @@ const getMenuPosition = (selectBase) => { return selectBase.getBoundingClientRect(); }; -const checkScrollVisibility = (scrollableParent, selectRoot, menuRoot) => { +const checkScrollVisibility = (scrollableParent: Element, selectRoot: Element, menuRoot: Element) => { const parentProportions = scrollableParent.getBoundingClientRect(); const rootProportions = selectRoot.getBoundingClientRect(); const menuProportions = menuRoot.getBoundingClientRect(); @@ -49,20 +50,44 @@ const checkScrollVisibility = (scrollableParent, selectRoot, menuRoot) => { }; }; -const MenuPortal = ({ selectToggleRef, menuPortalTarget, children }) => { +interface MenuPortalProps { + selectToggleRef: React.RefObject<HTMLDivElement>; + menuPortalTarget: Element; + children: React.ReactElement; +} + +const MenuPortal: React.FC<MenuPortalProps> = ({ selectToggleRef, menuPortalTarget, children }) => { const [position, setPosition] = useState(getMenuPosition(selectToggleRef.current)); - const [{ cropSize, rootPosition, maxHeight }, setCropSize] = useState({}); - const menuRef = useRef(); + const [{ cropSize, rootPosition, maxHeight }, setCropSize] = useState<{ + cropSize?: number; + rootPosition?: number; + maxHeight?: number; + }>({}); + const menuRef = useRef<HTMLDivElement>(null); + useEffect(() => { + if (!menuRef.current) { + return; + } + setCropSize({ maxHeight: window.innerHeight - menuRef.current.getBoundingClientRect().top - 4 }); - const scrollParentElement = getScrollParent(selectToggleRef.current); + const scrollParentElement = getScrollParent(selectToggleRef.current!); + const scrollHandler = function () { + if (!menuRef.current || !selectToggleRef.current) { + return; + } + setCropSize(checkScrollVisibility(scrollParentElement, selectToggleRef.current, menuRef.current)); setPosition(getMenuPosition(selectToggleRef.current)); }; const resizeHandler = function () { - setCropSize((prevSize) => ({ ...prevSize, maxHeight: window.innerHeight - menuRef.current.getBoundingClientRect().top - 4 })); + if (!menuRef.current || !selectToggleRef.current) { + return; + } + + setCropSize((prevSize) => ({ ...prevSize, maxHeight: window.innerHeight - menuRef.current!.getBoundingClientRect().top - 4 })); setPosition(getMenuPosition(selectToggleRef.current)); }; @@ -74,13 +99,15 @@ const MenuPortal = ({ selectToggleRef, menuPortalTarget, children }) => { }; }, [selectToggleRef]); - const top = position.top + position.height; + const top = (position as any).top + (position as any).height; const sizedMenu = React.cloneElement(children, { + // @ts-ignore style: { - maxHeight: cropSize < 0 ? maxHeight + cropSize : maxHeight, + maxHeight: (cropSize || 0) < 0 ? (maxHeight || 0) + (cropSize || 0) : maxHeight, overflow: 'auto', }, }); + const portalDiv = ( <div ref={menuRef} @@ -88,20 +115,39 @@ const MenuPortal = ({ selectToggleRef, menuPortalTarget, children }) => { style={{ zIndex: 401, position: 'absolute', - top: cropSize < 0 ? rootPosition : top, - left: position.left, - width: position.width, + top: (cropSize || 0) < 0 ? rootPosition : top, + left: (position as any).left, + width: (position as any).width, overflow: 'hidden', }} > - {cropSize < 0 ? <div style={{ position: 'relative', top: cropSize, width: position.width }}>{sizedMenu}</div> : sizedMenu} + {(cropSize || 0) < 0 ? <div style={{ position: 'relative', top: cropSize, width: (position as any).width }}>{sizedMenu}</div> : sizedMenu} </div> ); return createPortal(portalDiv, menuPortalTarget); }; -const Menu = ({ +interface MenuProps { + noResultsMessage?: string; + noOptionsMessage?: string; + filterOptions?: (options: SelectOption[], filterValue?: any) => SelectOption[]; + isSearchable?: boolean; + filterValue?: any; + getItemProps?: (props: any) => any; + getInputProps?: (props?: any) => any; + highlightedIndex?: number; + selectedItem?: any; + isMulti?: boolean; + isFetching?: boolean; + menuPortalTarget?: Element; + menuIsPortal?: boolean; + selectToggleRef?: React.RefObject<HTMLDivElement>; + originalOptions?: SelectOption[]; + options?: SelectOption[]; +} + +const Menu: React.FC<MenuProps> = ({ noResultsMessage, noOptionsMessage, filterOptions, @@ -116,23 +162,25 @@ const Menu = ({ menuPortalTarget, menuIsPortal, selectToggleRef, - originalOptions, + originalOptions = [], }) => { - const filteredOptions = isSearchable ? filterOptions(originalOptions, filterValue) : originalOptions; + const filteredOptions = isSearchable ? filterOptions?.(originalOptions, filterValue) || [] : originalOptions; let index = 0; - const createOption = (item) => { + const createOption = (item: SelectOption) => { index++; - const itemProps = getItemProps({ - item, - index, - isActive: highlightedIndex === index, - isSelected: isMulti ? !!selectedItem.find(({ value }) => item.value === value) : selectedItem === item.value, - onMouseUp: (e) => e.stopPropagation(), // we need this to prevent issues with portal menu not selecting a option - }); - let key = item.key; + const itemProps = + getItemProps?.({ + item, + index, + isActive: highlightedIndex === index, + isSelected: isMulti ? !!selectedItem?.find?.(({ value }: any) => item.value === value) : selectedItem === item.value, + onMouseUp: (e: React.MouseEvent) => e.stopPropagation(), // we need this to prevent issues with portal menu not selecting a option + }) || {}; + + let key = (item as any).key; if (!key && typeof item.value === 'object') { try { key = JSON.stringify(item.value); @@ -158,16 +206,16 @@ const Menu = ({ /> )} {filteredOptions.map((item, arrayIndex) => { - if (item.options) { + if ((item as any).options) { return ( <div className="pf-v6-c-menu__group" key={`group-${arrayIndex}`}> <div className="pf-v6-c-menu__group-title">{item.label}</div> - {item.options.map((nestedItem) => createOption(nestedItem))} + {(item as any).options.map((nestedItem: SelectOption) => createOption(nestedItem))} </div> ); } - if (item.divider) { + if ((item as any).divider) { return <hr className="pf-v6-c-divider" key={`divider-${index}`} />; } @@ -175,7 +223,8 @@ const Menu = ({ })} </ul> ); - if (menuIsPortal) { + + if (menuIsPortal && menuPortalTarget && selectToggleRef) { return ( <MenuPortal menuPortalTarget={menuPortalTarget} selectToggleRef={selectToggleRef}> {menuItems} diff --git a/packages/pf4-component-mapper/src/select/select/option.js b/packages/pf4-component-mapper/src/select/select/option.tsx similarity index 100% rename from packages/pf4-component-mapper/src/select/select/option.js rename to packages/pf4-component-mapper/src/select/select/option.tsx diff --git a/packages/pf4-component-mapper/src/select/select/select.js b/packages/pf4-component-mapper/src/select/select/select.tsx similarity index 77% rename from packages/pf4-component-mapper/src/select/select/select.js rename to packages/pf4-component-mapper/src/select/select/select.tsx index 1bf8742b8..8b172e344 100644 --- a/packages/pf4-component-mapper/src/select/select/select.js +++ b/packages/pf4-component-mapper/src/select/select/select.tsx @@ -15,8 +15,9 @@ import Menu from './menu'; import ClearIndicator from './clear-indicator'; import ValueContainer from './value-container'; import { Icon } from '@patternfly/react-core'; +import { SelectOption } from '../../types'; -const itemToString = (value, isMulti, showMore, handleShowMore, handleChange) => { +const customItemToString = (value: any, isMulti: boolean, showMore: boolean, handleShowMore: () => void, handleChange: (item: any) => void): any => { if (!value) { return ''; } @@ -31,7 +32,7 @@ const itemToString = (value, isMulti, showMore, handleShowMore, handleChange) => return ( <div className="pf-v6-c-label-group pf-v6-u-ml-sm" onClick={(event) => event.stopPropagation()}> <ul className="pf-v6-c-label-group__list" aria-label="Chip group category"> - {visibleOptions.map((item, index) => { + {visibleOptions.map((item: any, index: number) => { const label = typeof item === 'object' ? item.label : item; return ( <li className="pf-v6-c-label-group__list-item" onClick={(event) => event.stopPropagation()} key={item.key || item.value || item}> @@ -63,7 +64,7 @@ const itemToString = (value, isMulti, showMore, handleShowMore, handleChange) => ); } - return value.map((item) => (typeof item === 'object' ? item.label : item)); + return value.map((item: any) => (typeof item === 'object' ? item.label : item)); } if (typeof value === 'object') { @@ -73,8 +74,7 @@ const itemToString = (value, isMulti, showMore, handleShowMore, handleChange) => return value; }; -// TODO fix the value of internal select not to be an array all the time. It forces the filter value to be an array and it crashes sometimes. -const filterOptions = (options, filterValue = '') => { +const filterOptions = (options: SelectOption[], filterValue: any = ''): SelectOption[] => { const filter = (Array.isArray(filterValue) && filterValue.length > 0 ? filterValue[0] : filterValue).toLowerCase(); if (!filter) { @@ -84,7 +84,9 @@ const filterOptions = (options, filterValue = '') => { return options .map((option) => { if (option.options) { - const filteredNested = option.options.map((option) => (option.label?.toLowerCase().includes(filter) ? option : null)).filter(Boolean); + const filteredNested = option.options + .map((option) => (option.label?.toLowerCase().includes(filter) ? option : null)) + .filter(Boolean) as SelectOption[]; if (filteredNested.length === 0) { return null; @@ -102,25 +104,25 @@ const filterOptions = (options, filterValue = '') => { return null; }) - .filter(Boolean); + .filter(Boolean) as SelectOption[]; }; -const getValue = (isMulti, option, value) => { +const getValue = (isMulti: boolean, option: any, value: any): any => { if (!isMulti || !option) { return option; } - const isSelected = value.find(({ value }) => value === option.value); - return isSelected ? value.filter(({ value }) => value !== option.value) : [...value, option]; + const isSelected = value.find(({ value: itemValue }: any) => itemValue === option.value); + return isSelected ? value.filter(({ value: itemValue }: any) => itemValue !== option.value) : [...value, option]; }; -const stateReducer = (state, changes, isMulti) => { +const stateReducer = (state: any, changes: any, isMulti: boolean): any => { switch (changes.type) { case Downshift.stateChangeTypes.clickButton: return { ...state, ...changes, - highlightedIndex: undefined, // reset the item focus to prevent initial scroll and portal menu warping + highlightedIndex: undefined, inputValue: undefined, }; case Downshift.stateChangeTypes.keyDownEnter: @@ -129,7 +131,7 @@ const stateReducer = (state, changes, isMulti) => { ...changes, isOpen: isMulti ? state.isOpen : !state.isOpen, highlightedIndex: state.highlightedIndex, - inputValue: isMulti ? state.inputValue : changes.inputValue, // prevent filter value change after option click + inputValue: isMulti ? state.inputValue : changes.inputValue, }; case Downshift.stateChangeTypes.controlledPropUpdatedSelectedItem: return { @@ -167,7 +169,30 @@ const stateReducer = (state, changes, isMulti) => { } }; -const InternalSelect = ({ +export interface InternalSelectProps { + noResultsMessage?: string; + noOptionsMessage?: string; + onChange: (value: any) => void; + options: SelectOption[]; + value: any; + simpleValue?: boolean; + placeholder?: string; + isSearchable?: boolean; + isDisabled?: boolean; + isClearable?: boolean; + isMulti?: boolean; + isFetching?: boolean; + onInputChange?: (value: string) => void; + loadingMessage?: string; + menuPortalTarget?: Element; + menuIsPortal?: boolean; + originalOptions?: SelectOption[]; + id?: string; + name?: string; + [key: string]: any; +} + +const InternalSelect: React.FC<InternalSelectProps> = ({ noResultsMessage, noOptionsMessage, onChange, @@ -188,22 +213,20 @@ const InternalSelect = ({ ...props }) => { const [showMore, setShowMore] = useState(false); - const inputRef = useRef(); - const selectToggleRef = useRef(); + const inputRef = useRef<HTMLInputElement>(null); + const selectToggleRef = useRef<HTMLDivElement>(null); const parsedValue = parseInternalValue(value); const handleShowMore = () => setShowMore((prev) => !prev); - const handleChange = (option) => onChange(getValue(isMulti, option, value)); + const handleChange = (option: any) => onChange(getValue(isMulti || false, option, value)); + return ( <Downshift id={props.id || props.name} onChange={handleChange} - itemToString={(value) => itemToString(value, isMulti, showMore, handleShowMore, handleChange)} + itemToString={(value) => customItemToString(value, isMulti || false, showMore, handleShowMore, handleChange)} selectedItem={value || ''} - stateReducer={(state, changes) => stateReducer(state, changes, isMulti)} + stateReducer={(state, changes) => stateReducer(state, changes, isMulti || false)} onInputValueChange={(inputValue, { selectedItem }) => { - /** - * Prevent firing te load options callback when selecting value not filtering - */ if (onInputChange && typeof inputValue === 'string' && selectedItem?.label !== inputValue) { onInputChange(inputValue); } @@ -215,7 +238,6 @@ const InternalSelect = ({ <div className="pf-v6-c-menu"> <div ref={selectToggleRef} - disabled={isDisabled} className={`pf-v6-c-menu-toggle${isDisabled ? ' pf-m-disabled' : ''}${ isSearchable ? ' pf-m-typeahead' : '' } ddorg__pf4-component-mapper__select-toggle`} @@ -228,7 +250,7 @@ const InternalSelect = ({ placeholder={placeholder} inputRef={inputRef} getInputProps={getInputProps} - value={itemToString(selectedItem, isMulti, showMore, handleShowMore, handleChange)} + value={customItemToString(selectedItem, isMulti || false, showMore, handleShowMore, handleChange)} /> {isClearable && parsedValue && <ClearIndicator clearSelection={clearSelection} />} <button className="pf-v6-c-menu-toggle__button"> @@ -244,7 +266,6 @@ const InternalSelect = ({ noResultsMessage={noResultsMessage} noOptionsMessage={noOptionsMessage} isFetching={isFetching} - isDisabled={isDisabled} isSearchable={isSearchable} getInputProps={getInputProps} filterOptions={filterOptions} @@ -267,7 +288,23 @@ const InternalSelect = ({ ); }; -const Select = ({ +export interface SelectComponentProps { + showMoreLabel?: string; + showLessLabel?: string; + simpleValue?: boolean; + loadingMessage?: string; + updatingMessage?: string; + options?: SelectOption[]; + menuIsPortal?: boolean; + placeholder?: string; + isSearchable?: boolean; + isClearable?: boolean; + noResultsMessage?: string; + noOptionsMessage?: string; + [key: string]: any; +} + +const Select: React.FC<SelectComponentProps> = ({ showMoreLabel = 'more', showLessLabel = 'Show less', simpleValue = true, diff --git a/packages/pf4-component-mapper/src/select/select/value-container.js b/packages/pf4-component-mapper/src/select/select/value-container.tsx similarity index 61% rename from packages/pf4-component-mapper/src/select/select/value-container.js rename to packages/pf4-component-mapper/src/select/select/value-container.tsx index 593028b5c..d79834802 100644 --- a/packages/pf4-component-mapper/src/select/select/value-container.js +++ b/packages/pf4-component-mapper/src/select/select/value-container.tsx @@ -1,7 +1,16 @@ import React, { Fragment } from 'react'; import Input from './input'; -const ValueContainer = ({ value, isMulti, placeholder, getInputProps, isSearchable, inputRef }) => { +interface ValueContainerProps { + value: any; + isMulti?: boolean; + placeholder?: string; + getInputProps: (props?: any) => any; + isSearchable?: boolean; + inputRef: React.RefObject<HTMLInputElement>; +} + +const ValueContainer: React.FC<ValueContainerProps> = ({ value, isMulti, placeholder, getInputProps, isSearchable, inputRef }) => { if (isMulti && isSearchable) { return ( <Fragment> diff --git a/packages/pf4-component-mapper/src/show-error/index.js b/packages/pf4-component-mapper/src/show-error/index.js deleted file mode 100644 index 86c01f23d..000000000 --- a/packages/pf4-component-mapper/src/show-error/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './show-error'; -export * from './show-error'; diff --git a/packages/pf4-component-mapper/src/show-error/index.d.ts b/packages/pf4-component-mapper/src/show-error/index.ts similarity index 100% rename from packages/pf4-component-mapper/src/show-error/index.d.ts rename to packages/pf4-component-mapper/src/show-error/index.ts diff --git a/packages/pf4-component-mapper/src/show-error/show-error.d.ts b/packages/pf4-component-mapper/src/show-error/show-error.d.ts deleted file mode 100644 index b1a24463a..000000000 --- a/packages/pf4-component-mapper/src/show-error/show-error.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Meta } from "@data-driven-forms/react-form-renderer"; - -declare function showError(meta: Meta<any>, validateOnMount?: boolean): { validated: 'error' | 'warning' | 'default' } - -export default showError; diff --git a/packages/pf4-component-mapper/src/show-error/show-error.js b/packages/pf4-component-mapper/src/show-error/show-error.js deleted file mode 100644 index 5e6ed7a5e..000000000 --- a/packages/pf4-component-mapper/src/show-error/show-error.js +++ /dev/null @@ -1,17 +0,0 @@ -const showError = ({ error, touched, warning, submitError }, validateOnMount) => { - if ((touched || validateOnMount) && error) { - return { validated: 'error' }; - } - - if ((touched || validateOnMount) && submitError) { - return { validated: 'error' }; - } - - if ((touched || validateOnMount) && warning) { - return { validated: 'warning' }; - } - - return { validated: 'default' }; -}; - -export default showError; diff --git a/packages/pf4-component-mapper/src/show-error/show-error.ts b/packages/pf4-component-mapper/src/show-error/show-error.ts new file mode 100644 index 000000000..e6e104332 --- /dev/null +++ b/packages/pf4-component-mapper/src/show-error/show-error.ts @@ -0,0 +1,22 @@ +import { FieldMeta, ShowErrorResult, ValidationState } from '../types'; + +/** + * Determines validation state based on field meta information + */ +const showError = (meta: FieldMeta, validateOnMount?: boolean): ShowErrorResult => { + if ((meta.touched || validateOnMount) && meta.error) { + return { validated: 'error' as ValidationState }; + } + + if ((meta.touched || validateOnMount) && meta.submitError) { + return { validated: 'error' as ValidationState }; + } + + if ((meta.touched || validateOnMount) && meta.warning) { + return { validated: 'warning' as ValidationState }; + } + + return { validated: 'default' as ValidationState }; +}; + +export default showError; diff --git a/packages/pf4-component-mapper/src/slider/index.js b/packages/pf4-component-mapper/src/slider/index.js deleted file mode 100644 index 67fbfdaf0..000000000 --- a/packages/pf4-component-mapper/src/slider/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './slider'; -export * from './slider'; diff --git a/packages/pf4-component-mapper/src/slider/index.d.ts b/packages/pf4-component-mapper/src/slider/index.tsx similarity index 58% rename from packages/pf4-component-mapper/src/slider/index.d.ts rename to packages/pf4-component-mapper/src/slider/index.tsx index 67fbfdaf0..dca2335b1 100644 --- a/packages/pf4-component-mapper/src/slider/index.d.ts +++ b/packages/pf4-component-mapper/src/slider/index.tsx @@ -1,2 +1 @@ export { default } from './slider'; -export * from './slider'; diff --git a/packages/pf4-component-mapper/src/slider/slider.d.ts b/packages/pf4-component-mapper/src/slider/slider.d.ts deleted file mode 100644 index 29bb690e5..000000000 --- a/packages/pf4-component-mapper/src/slider/slider.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { UseFieldApiComponentConfig } from "@data-driven-forms/react-form-renderer"; -import FormGroupProps from "../form-group"; - -interface InternalSliderProps extends React.HTMLProps<React.InputHTMLAttributes<any>> { - isReadOnly?: boolean; -} - -export type SliderProps = InternalSliderProps & FormGroupProps & UseFieldApiComponentConfig; - -declare const Slider: React.ComponentType<SliderProps>; - -export default Slider; diff --git a/packages/pf4-component-mapper/src/slider/slider.js b/packages/pf4-component-mapper/src/slider/slider.tsx similarity index 87% rename from packages/pf4-component-mapper/src/slider/slider.js rename to packages/pf4-component-mapper/src/slider/slider.tsx index 882a686e7..7d99b5e9a 100644 --- a/packages/pf4-component-mapper/src/slider/slider.js +++ b/packages/pf4-component-mapper/src/slider/slider.tsx @@ -2,8 +2,9 @@ import React from 'react'; import { useFieldApi } from '@data-driven-forms/react-form-renderer'; import FormGroup from '../form-group/form-group'; import { Slider as PF4Slider } from '@patternfly/react-core'; +import { BaseFieldProps, SliderProps } from '../types'; -const Slider = (props) => { +const Slider: React.FC<BaseFieldProps<SliderProps>> = (props) => { const { label, isRequired, helperText, meta, validateOnMount, description, input, isReadOnly, isDisabled, id, FormGroupProps, ...rest } = useFieldApi(props); diff --git a/packages/pf4-component-mapper/src/sub-form/index.js b/packages/pf4-component-mapper/src/sub-form/index.js deleted file mode 100644 index e5c265b70..000000000 --- a/packages/pf4-component-mapper/src/sub-form/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './sub-form'; -export * from './sub-form'; diff --git a/packages/pf4-component-mapper/src/sub-form/index.d.ts b/packages/pf4-component-mapper/src/sub-form/index.tsx similarity index 57% rename from packages/pf4-component-mapper/src/sub-form/index.d.ts rename to packages/pf4-component-mapper/src/sub-form/index.tsx index e5c265b70..458549228 100644 --- a/packages/pf4-component-mapper/src/sub-form/index.d.ts +++ b/packages/pf4-component-mapper/src/sub-form/index.tsx @@ -1,2 +1 @@ export { default } from './sub-form'; -export * from './sub-form'; diff --git a/packages/pf4-component-mapper/src/sub-form/sub-form.d.ts b/packages/pf4-component-mapper/src/sub-form/sub-form.d.ts deleted file mode 100644 index 773078912..000000000 --- a/packages/pf4-component-mapper/src/sub-form/sub-form.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Field } from "@data-driven-forms/react-form-renderer"; -import { ReactNode } from "react"; - -export interface SubFormProps { - fields: Field[]; - name: string; - title?: ReactNode; - description?: ReactNode; -} - -declare const SubForm: React.ComponentType<SubFormProps>; - -export default SubForm; diff --git a/packages/pf4-component-mapper/src/sub-form/sub-form.js b/packages/pf4-component-mapper/src/sub-form/sub-form.tsx similarity index 75% rename from packages/pf4-component-mapper/src/sub-form/sub-form.js rename to packages/pf4-component-mapper/src/sub-form/sub-form.tsx index aa59aac5c..12487e7fa 100644 --- a/packages/pf4-component-mapper/src/sub-form/sub-form.js +++ b/packages/pf4-component-mapper/src/sub-form/sub-form.tsx @@ -2,8 +2,9 @@ import React from 'react'; import { useFormApi } from '@data-driven-forms/react-form-renderer'; import { Title, Grid, GridItem, Content, ContentVariants } from '@patternfly/react-core'; +import { BaseFieldProps, SubFormProps } from '../types'; -const SubForm = ({ fields, title, description, validate: _validate, component, ...rest }) => { +const SubForm: React.FC<BaseFieldProps<SubFormProps>> = ({ fields, title, description, validate: _validate, component, ...rest }) => { const formOptions = useFormApi(); return ( @@ -24,7 +25,7 @@ const SubForm = ({ fields, title, description, validate: _validate, component, . </Content> </GridItem> )} - {formOptions.renderForm(fields, formOptions)} + {formOptions.renderForm(fields)} </Grid> ); }; diff --git a/packages/pf4-component-mapper/src/switch/index.js b/packages/pf4-component-mapper/src/switch/index.js deleted file mode 100644 index 064e75db6..000000000 --- a/packages/pf4-component-mapper/src/switch/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './switch'; -export * from './switch'; diff --git a/packages/pf4-component-mapper/src/switch/index.d.ts b/packages/pf4-component-mapper/src/switch/index.ts similarity index 100% rename from packages/pf4-component-mapper/src/switch/index.d.ts rename to packages/pf4-component-mapper/src/switch/index.ts diff --git a/packages/pf4-component-mapper/src/switch/switch.d.ts b/packages/pf4-component-mapper/src/switch/switch.d.ts deleted file mode 100644 index 6e97515f7..000000000 --- a/packages/pf4-component-mapper/src/switch/switch.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -import FormGroupProps from "../form-group"; -import { UseFieldApiComponentConfig } from "@data-driven-forms/react-form-renderer"; -import { SwitchProps as PFSwitchProps } from '@patternfly/react-core'; - -interface InternalSwitchProps extends PFSwitchProps { - isReadOnly?: boolean; -} - -export type SwitchProps = InternalSwitchProps & FormGroupProps & UseFieldApiComponentConfig; - -declare const Switch: React.ComponentType<SwitchProps>; - -export default Switch; diff --git a/packages/pf4-component-mapper/src/switch/switch.js b/packages/pf4-component-mapper/src/switch/switch.tsx similarity index 70% rename from packages/pf4-component-mapper/src/switch/switch.js rename to packages/pf4-component-mapper/src/switch/switch.tsx index f5b418236..712f39497 100644 --- a/packages/pf4-component-mapper/src/switch/switch.js +++ b/packages/pf4-component-mapper/src/switch/switch.tsx @@ -1,15 +1,21 @@ import React from 'react'; -import { Switch as Pf4Switch } from '@patternfly/react-core'; +import { Switch as Pf4Switch, SwitchProps } from '@patternfly/react-core'; import { useFieldApi } from '@data-driven-forms/react-form-renderer'; import FormGroup from '../form-group/form-group'; import IsRequired from '../is-required/is-required'; +import { BaseFieldProps } from '../types'; -const Switch = (props) => { +export interface SwitchFieldProps extends BaseFieldProps, Omit<SwitchProps, keyof BaseFieldProps | 'id' | 'isDisabled'> { + onText?: string; +} + +const Switch: React.FC<SwitchFieldProps> = (props) => { const { label, onText, isRequired, helperText, meta, validateOnMount, description, input, isReadOnly, isDisabled, id, FormGroupProps, ...rest } = useFieldApi({ ...props, type: 'checkbox', }); + return ( <FormGroup isRequired={isRequired} @@ -33,3 +39,6 @@ const Switch = (props) => { }; export default Switch; + +// Export the props type for external use +export type { SwitchProps } from '../types'; diff --git a/packages/pf4-component-mapper/src/tabs/index.js b/packages/pf4-component-mapper/src/tabs/index.js deleted file mode 100644 index ee0f9467c..000000000 --- a/packages/pf4-component-mapper/src/tabs/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './tabs'; -export * from './tabs'; diff --git a/packages/pf4-component-mapper/src/tabs/index.d.ts b/packages/pf4-component-mapper/src/tabs/index.tsx similarity index 58% rename from packages/pf4-component-mapper/src/tabs/index.d.ts rename to packages/pf4-component-mapper/src/tabs/index.tsx index ee0f9467c..203dd523c 100644 --- a/packages/pf4-component-mapper/src/tabs/index.d.ts +++ b/packages/pf4-component-mapper/src/tabs/index.tsx @@ -1,2 +1 @@ export { default } from './tabs'; -export * from './tabs'; diff --git a/packages/pf4-component-mapper/src/tabs/tabs.d.ts b/packages/pf4-component-mapper/src/tabs/tabs.d.ts deleted file mode 100644 index 45d61ca19..000000000 --- a/packages/pf4-component-mapper/src/tabs/tabs.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Field } from "@data-driven-forms/react-form-renderer"; -import { ReactNode } from "react"; - -export interface TabField { - fields: Field[]; - name: string; - title?: ReactNode; -} - -export interface TabsProps { - fields: TabField[]; - name: string; -} - -declare const Tabs: React.ComponentType<TabsProps>; - -export default Tabs; diff --git a/packages/pf4-component-mapper/src/tabs/tabs.js b/packages/pf4-component-mapper/src/tabs/tabs.tsx similarity index 55% rename from packages/pf4-component-mapper/src/tabs/tabs.js rename to packages/pf4-component-mapper/src/tabs/tabs.tsx index 637a00989..dca558d87 100644 --- a/packages/pf4-component-mapper/src/tabs/tabs.js +++ b/packages/pf4-component-mapper/src/tabs/tabs.tsx @@ -2,26 +2,27 @@ import React, { useState } from 'react'; import { useFormApi } from '@data-driven-forms/react-form-renderer'; import { Tab, Tabs, TabTitleText } from '@patternfly/react-core'; +import { BaseFieldProps, TabsProps, TabField } from '../types'; -const FormTabs = ({ fields, dataType, validate, component, ...rest }) => { +const FormTabs: React.FC<BaseFieldProps<TabsProps>> = ({ fields, dataType, validate, component, ...rest }) => { const formOptions = useFormApi(); - const [activeTabKey, setActiveTabKey] = useState(0); + const [activeTabKey, setActiveTabKey] = useState<number>(0); - const handleTabClick = (event, tabIndex) => { + const handleTabClick = (event: React.MouseEvent<HTMLElement>, tabIndex: string | number) => { event.preventDefault(); - setActiveTabKey(tabIndex); + setActiveTabKey(typeof tabIndex === 'string' ? parseInt(tabIndex, 10) : tabIndex); }; - const renderTabItems = (fields) => + const renderTabItems = (fields: TabField[]) => fields.map(({ fields, title, name }, index) => ( <Tab key={name} eventKey={index} title={typeof title === 'string' ? <TabTitleText>{title}</TabTitleText> : title}> - <div className="pf-c-form">{formOptions.renderForm(fields, formOptions)}</div> + <div className="pf-c-form">{formOptions.renderForm(fields)}</div> </Tab> )); return ( <Tabs activeKey={activeTabKey} onSelect={handleTabClick} {...rest}> - {renderTabItems(fields, formOptions)} + {renderTabItems(fields)} </Tabs> ); }; diff --git a/packages/pf4-component-mapper/src/tests/select/select.test.js b/packages/pf4-component-mapper/src/tests/select/select.test.js index fcaa9f731..e9c4cc270 100644 --- a/packages/pf4-component-mapper/src/tests/select/select.test.js +++ b/packages/pf4-component-mapper/src/tests/select/select.test.js @@ -204,7 +204,7 @@ describe('<Select />', () => { expect(screen.getByText('Category 2')).toHaveClass('pf-v6-c-menu__group-title'); expect(container.getElementsByClassName('pf-v6-c-divider')).toHaveLength(0); - expect([...container.getElementsByClassName('pf-v6-c-menu__item')].map((opt) => opt.textContent)).toEqual([ + expect([...container.getElementsByClassName('pf-v6-c-menu__list-item')].map((opt) => opt.textContent)).toEqual([ 'value 1', 'value 2', 'value 3', @@ -216,7 +216,10 @@ describe('<Select />', () => { expect(container.getElementsByClassName('pf-v6-c-divider')).toHaveLength(0); expect(container.getElementsByClassName('pf-v6-c-menu__group-title')).toHaveLength(0); - expect([...container.getElementsByClassName('pf-v6-c-menu__item')].map((opt) => opt.textContent)).toEqual(['independent 1', 'independent 2']); + expect([...container.getElementsByClassName('pf-v6-c-menu__list-item')].map((opt) => opt.textContent)).toEqual([ + 'independent 1', + 'independent 2', + ]); }); it('should return single simple value', async () => { @@ -354,19 +357,21 @@ describe('<Select />', () => { const asyncLoading = jest.fn().mockReturnValue(Promise.resolve([{ label: 'label', value: '123' }])); const onChange = jest.fn(); - render( - <Select - {...initialProps} - value={[{ value: '123', label: 'label' }, 'Not in options']} - isMulti - options={undefined} - loadOptions={asyncLoading} - onChange={onChange} - simpleValue - /> + await waitFor(() => + render( + <Select + {...initialProps} + value={[{ value: '123', label: 'label' }, 'Not in options']} + isMulti + options={undefined} + loadOptions={asyncLoading} + onChange={onChange} + simpleValue + /> + ) ); - await userEvent.click(screen.getByLabelText('open menu')); + await waitFor(() => userEvent.click(screen.getByLabelText('open menu'))); await waitFor(() => expect(screen.getByText('label', { selector: '.pf-v6-c-menu__item-main' })).toBeInTheDocument()); expect(onChange).toHaveBeenCalledWith([{ label: 'label', value: '123' }]); @@ -407,7 +412,7 @@ describe('<Select />', () => { await userEvent.click(screen.getByLabelText('open menu')); - expect([...container.getElementsByClassName('pf-v6-c-menu__item')].map((opt) => opt.textContent)).toEqual( + expect([...container.getElementsByClassName('pf-v6-c-menu__list-item')].map((opt) => opt.textContent)).toEqual( initialProps.options.map((opt) => opt.label) ); @@ -425,7 +430,7 @@ describe('<Select />', () => { initialProps.options.map((opt) => opt.label) ); - await act(async () => { + await waitFor(async () => { rerender(<Select {...initialProps} loadOptions={asyncLoadingNew} />); }); diff --git a/packages/pf4-component-mapper/src/text-field/index.js b/packages/pf4-component-mapper/src/text-field/index.js deleted file mode 100644 index 7a43eb624..000000000 --- a/packages/pf4-component-mapper/src/text-field/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './text-field'; -export * from './text-field'; diff --git a/packages/pf4-component-mapper/src/text-field/index.d.ts b/packages/pf4-component-mapper/src/text-field/index.ts similarity index 100% rename from packages/pf4-component-mapper/src/text-field/index.d.ts rename to packages/pf4-component-mapper/src/text-field/index.ts diff --git a/packages/pf4-component-mapper/src/text-field/text-field.d.ts b/packages/pf4-component-mapper/src/text-field/text-field.d.ts deleted file mode 100644 index 5367fa8cf..000000000 --- a/packages/pf4-component-mapper/src/text-field/text-field.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -import FormGroupProps from "../form-group" -import { UseFieldApiComponentConfig } from "@data-driven-forms/react-form-renderer" -import { TextInputProps } from "@patternfly/react-core"; - -interface InternalTextFieldProps extends TextInputProps {} - -export type TextFieldProps = InternalTextFieldProps & FormGroupProps & UseFieldApiComponentConfig; - -declare const TextField: React.ComponentType<TextFieldProps>; - -export default TextField; diff --git a/packages/pf4-component-mapper/src/text-field/text-field.js b/packages/pf4-component-mapper/src/text-field/text-field.tsx similarity index 61% rename from packages/pf4-component-mapper/src/text-field/text-field.js rename to packages/pf4-component-mapper/src/text-field/text-field.tsx index e32b5225f..6013b492b 100644 --- a/packages/pf4-component-mapper/src/text-field/text-field.js +++ b/packages/pf4-component-mapper/src/text-field/text-field.tsx @@ -1,12 +1,30 @@ import React from 'react'; -import FormGroup from '../form-group/form-group'; import { TextInput } from '@patternfly/react-core'; import { useFieldApi } from '@data-driven-forms/react-form-renderer'; + +import FormGroup from '../form-group/form-group'; import showError from '../show-error/show-error'; +import { BaseFieldProps, TextFieldProps } from '../types'; + +const TextField = (props: BaseFieldProps<TextFieldProps>) => { + const { + label, + isRequired, + helperText, + meta, + validateOnMount, + description, + hideLabel, + input, + isReadOnly, + isDisabled, + id, + FormGroupProps, + placeholder, + type = 'text', + ...rest + } = useFieldApi(props); -const TextField = (props) => { - const { label, isRequired, helperText, meta, validateOnMount, description, hideLabel, input, isReadOnly, isDisabled, id, FormGroupProps, ...rest } = - useFieldApi(props); return ( <FormGroup label={label} @@ -27,9 +45,14 @@ const TextField = (props) => { isRequired={isRequired} isDisabled={isDisabled} readOnlyVariant={isReadOnly ? 'default' : undefined} + placeholder={placeholder} + type={type} /> </FormGroup> ); }; export default TextField; + +// Export the props type for external use +export type { TextFieldProps } from '../types'; diff --git a/packages/pf4-component-mapper/src/textarea/index.js b/packages/pf4-component-mapper/src/textarea/index.js deleted file mode 100644 index a3366cc62..000000000 --- a/packages/pf4-component-mapper/src/textarea/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './textarea'; -export * from './textarea'; diff --git a/packages/pf4-component-mapper/src/textarea/index.d.ts b/packages/pf4-component-mapper/src/textarea/index.ts similarity index 100% rename from packages/pf4-component-mapper/src/textarea/index.d.ts rename to packages/pf4-component-mapper/src/textarea/index.ts diff --git a/packages/pf4-component-mapper/src/textarea/textarea.d.ts b/packages/pf4-component-mapper/src/textarea/textarea.d.ts deleted file mode 100644 index 50dca958d..000000000 --- a/packages/pf4-component-mapper/src/textarea/textarea.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -import FormGroupProps from "../form-group"; -import { UseFieldApiComponentConfig } from "@data-driven-forms/react-form-renderer"; -import { TextAreaProps as PfTextAreaProps } from '@patternfly/react-core'; - -interface InternalTextareaProps extends PfTextAreaProps {} - -export type TextareaProps = InternalTextareaProps & FormGroupProps & UseFieldApiComponentConfig; - -declare const Textarea: React.ComponentType<TextareaProps>; - -export default Textarea; diff --git a/packages/pf4-component-mapper/src/textarea/textarea.js b/packages/pf4-component-mapper/src/textarea/textarea.tsx similarity index 72% rename from packages/pf4-component-mapper/src/textarea/textarea.js rename to packages/pf4-component-mapper/src/textarea/textarea.tsx index 3aec7bda9..1f5b6b07a 100644 --- a/packages/pf4-component-mapper/src/textarea/textarea.js +++ b/packages/pf4-component-mapper/src/textarea/textarea.tsx @@ -1,12 +1,16 @@ import React from 'react'; -import { TextArea as Pf4TextArea } from '@patternfly/react-core'; +import { TextArea as Pf4TextArea, TextAreaProps } from '@patternfly/react-core'; import { useFieldApi } from '@data-driven-forms/react-form-renderer'; import FormGroup from '../form-group/form-group'; import showError from '../show-error/show-error'; +import { BaseFieldProps } from '../types'; -const Textarea = (props) => { +export interface TextareaProps extends BaseFieldProps, Omit<TextAreaProps, keyof BaseFieldProps | 'id' | 'isRequired' | 'isDisabled'> {} + +const Textarea: React.FC<TextareaProps> = (props) => { const { label, isRequired, helperText, meta, validateOnMount, description, hideLabel, input, isReadOnly, isDisabled, id, FormGroupProps, ...rest } = useFieldApi(props); + return ( <FormGroup label={label} @@ -32,3 +36,5 @@ const Textarea = (props) => { }; export default Textarea; + +// TextareaProps is exported above diff --git a/packages/pf4-component-mapper/src/time-picker/index.js b/packages/pf4-component-mapper/src/time-picker/index.js deleted file mode 100644 index c68e6eff2..000000000 --- a/packages/pf4-component-mapper/src/time-picker/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './time-picker'; -export * from './time-picker'; diff --git a/packages/pf4-component-mapper/src/time-picker/index.d.ts b/packages/pf4-component-mapper/src/time-picker/index.tsx similarity index 56% rename from packages/pf4-component-mapper/src/time-picker/index.d.ts rename to packages/pf4-component-mapper/src/time-picker/index.tsx index c68e6eff2..dc5c734ff 100644 --- a/packages/pf4-component-mapper/src/time-picker/index.d.ts +++ b/packages/pf4-component-mapper/src/time-picker/index.tsx @@ -1,2 +1 @@ export { default } from './time-picker'; -export * from './time-picker'; diff --git a/packages/pf4-component-mapper/src/time-picker/time-picker.d.ts b/packages/pf4-component-mapper/src/time-picker/time-picker.d.ts deleted file mode 100644 index 9ef469cec..000000000 --- a/packages/pf4-component-mapper/src/time-picker/time-picker.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { UseFieldApiComponentConfig } from "@data-driven-forms/react-form-renderer"; -import { TextInputProps } from "@patternfly/react-core"; -import FormGroupProps from "../form-group"; - -export type TimePickerProps = TextInputProps & FormGroupProps & UseFieldApiComponentConfig; - -declare const TimePicker: React.ComponentType<TimePickerProps>; - -export default TimePicker; diff --git a/packages/pf4-component-mapper/src/time-picker/time-picker.js b/packages/pf4-component-mapper/src/time-picker/time-picker.tsx similarity index 79% rename from packages/pf4-component-mapper/src/time-picker/time-picker.js rename to packages/pf4-component-mapper/src/time-picker/time-picker.tsx index b3c83dde1..c6f3604de 100644 --- a/packages/pf4-component-mapper/src/time-picker/time-picker.js +++ b/packages/pf4-component-mapper/src/time-picker/time-picker.tsx @@ -2,10 +2,12 @@ import React from 'react'; import FormGroup from '../form-group/form-group'; import { TimePicker as PF4TimePicker } from '@patternfly/react-core'; import { useFieldApi } from '@data-driven-forms/react-form-renderer'; +import { BaseFieldProps, TimePickerProps } from '../types'; -const TimePicker = (props) => { +const TimePicker: React.FC<BaseFieldProps<TimePickerProps>> = (props) => { const { label, isRequired, helperText, meta, validateOnMount, description, hideLabel, input, isReadOnly, isDisabled, id, FormGroupProps, ...rest } = useFieldApi(props); + return ( <FormGroup label={label} @@ -20,7 +22,7 @@ const TimePicker = (props) => { > <PF4TimePicker {...input} - onChange={(_e, value) => input.onChange(value)} + onChange={(_e: React.FormEvent<HTMLInputElement>, value: string) => input.onChange(value)} time={input.value ? input.value : undefined} {...rest} id={id || input.name} diff --git a/packages/pf4-component-mapper/src/types.ts b/packages/pf4-component-mapper/src/types.ts new file mode 100644 index 000000000..f31a4123c --- /dev/null +++ b/packages/pf4-component-mapper/src/types.ts @@ -0,0 +1,349 @@ +import { Dispatch, ReactNode } from 'react'; +import { ButtonVariant, FormGroupProps as PfFormGroupProps } from '@patternfly/react-core'; +import { BaseFieldProps } from '@data-driven-forms/react-form-renderer/use-field-api'; + +// Re-export for convenience +export type { BaseFieldProps }; + +/** + * Extended validation states for PatternFly components + */ +export type ValidationState = 'default' | 'success' | 'warning' | 'error'; + +/** + * Show error function return type + */ +export interface ShowErrorResult { + validated: ValidationState; +} + +/** + * Field meta state (from final-form) + */ +export interface FieldMeta { + error?: string; + touched?: boolean; + warning?: string; + submitError?: string; +} + +/** + * Field input props (from final-form) + */ +export interface FieldInput { + name: string; + value?: any; + onChange: (value: any) => void; + onBlur: (event?: any) => void; + onFocus: (event?: any) => void; + checked?: boolean; +} + +/** + * Select option type + */ +export interface SelectOption { + label: string; + value: any; + disabled?: boolean; + options?: SelectOption[]; + [key: string]: any; +} + +/** + * Props for FormGroup component + */ +export interface FormGroupComponentProps { + /** Field label */ + label?: ReactNode; + /** Whether field is required */ + isRequired?: boolean; + /** Helper text displayed below the field */ + helperText?: ReactNode; + /** Field description */ + description?: ReactNode; + /** Hide the field label */ + hideLabel?: boolean; + /** Field ID - falls back to input.name if not provided */ + id?: string; + /** Validate field on mount */ + validateOnMount?: boolean; + /** Field meta state */ + meta: FieldMeta; + /** Child components */ + children: ReactNode; + /** Additional FormGroup props */ + FormGroupProps?: Partial<PfFormGroupProps>; +} + +// ========== Component-specific Props Interfaces ========== + +/** + * Props for TextField component + */ +export interface TextFieldProps extends BaseFieldProps { + placeholder?: string; + type?: string; +} + +/** + * Props for Textarea component + */ +export interface TextareaProps extends BaseFieldProps { + placeholder?: string; + rows?: number; + resizeOrientation?: 'horizontal' | 'vertical' | 'both'; +} + +/** + * Props for Select component + */ +export interface SelectProps extends BaseFieldProps { + /** Select options */ + options?: SelectOption[]; + /** Placeholder text */ + placeholder?: string; + /** Enable search functionality */ + isSearchable?: boolean; + /** Enable multi-select */ + isMulti?: boolean; + /** Enable clear functionality */ + isClearable?: boolean; + /** Loading state */ + isFetching?: boolean; + /** No results message */ + noResultsMessage?: string; + /** No options message */ + noOptionsMessage?: string; + /** Loading message */ + loadingMessage?: string; + /** Updating message */ + updatingMessage?: string; + /** Show more label for multi-select */ + showMoreLabel?: string; + /** Show less label for multi-select */ + showLessLabel?: string; + /** Use simple values instead of option objects */ + simpleValue?: boolean; + /** Render menu in portal */ + menuIsPortal?: boolean; + /** Portal target element */ + menuPortalTarget?: Element; + /** Input change handler for search */ + onInputChange?: (value: string) => void; + /** Original options before transformation */ + originalOptions?: SelectOption[]; +} + +/** + * Props for Checkbox component + */ +export interface CheckboxProps extends BaseFieldProps { + /** Options for multi-choice checkbox */ + options?: SelectOption[]; +} + +/** + * Props for Radio component + */ +export interface RadioProps extends BaseFieldProps { + options: SelectOption[]; +} + +/** + * Props for Switch component + */ +export interface SwitchProps extends BaseFieldProps { + /** Text for the switch when on */ + onText?: string; + /** Text for the switch when off */ + offText?: string; +} + +/** + * Props for DatePicker component + */ +export interface DatePickerProps extends BaseFieldProps { + placeholder?: string; + format?: string; + locale?: string; +} + +/** + * Props for TimePicker component + */ +export interface TimePickerProps extends BaseFieldProps { + placeholder?: string; + format?: string; + is24Hour?: boolean; +} + +/** + * Props for PlainText component + */ +export interface PlainTextProps extends BaseFieldProps { + variant?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'small' | 'blockquote'; +} + +/** + * Props for Slider component + */ +export interface SliderProps extends BaseFieldProps { + min?: number; + max?: number; + step?: number; + showTicks?: boolean; +} + +/** + * Props for DualListSelect component + */ +export interface DualListSelectProps extends BaseFieldProps { + leftTitle?: string; + rightTitle?: string; + options?: SelectOption[]; + getValueFromNode?: (node: any) => any; + isSearchable?: boolean; + isSortable?: boolean; + isTree?: boolean; +} + +/** + * Button labels for FieldArray component + */ +export interface FieldArrayButtonLabels { + add?: string; + remove?: string; + removeAll?: string; +} + +/** + * Props for FieldArray component + */ +export interface FieldArrayProps extends BaseFieldProps { + /** Form fields to render in each array item */ + fields?: any[]; + /** Default item to add when creating new items */ + defaultItem?: any; + /** Minimum number of items allowed */ + minItems?: number; + /** Maximum number of items allowed */ + maxItems?: number; + /** Message displayed when no items exist */ + noItemsMessage?: string; + /** Custom labels for buttons */ + buttonLabels?: FieldArrayButtonLabels; + /** Array-level validation function */ + arrayValidator?: (values: any[]) => string | undefined; +} + +/** + * Tab field definition for Tabs component + */ +export interface TabField { + /** Tab title */ + title: string | ReactNode; + /** Tab unique identifier */ + name: string; + /** Fields to render in this tab */ + fields: any[]; +} + +/** + * Props for Tabs component + */ +export interface TabsProps extends BaseFieldProps { + /** Tab definitions */ + fields: TabField[]; +} + +/** + * Props for SubForm component + */ +export interface SubFormProps extends BaseFieldProps { + /** Form fields to render */ + fields: any[]; + /** Sub-form title */ + title?: string; + /** Sub-form description */ + description?: string; +} + +/** + * Props for form template Button component + */ +export interface FormTemplateButtonProps { + label?: string; + bsStyle?: ButtonVariant; + children?: ReactNode; + disabled?: boolean; + buttonType?: 'submit' | 'reset' | 'button' | 'cancel'; +} + +/** + * Props for form template ButtonGroup component + */ +export interface FormTemplateButtonGroupProps { + children: ReactNode; +} + +/** + * Props for form template Title component + */ +export interface FormTemplateTitleProps { + children: ReactNode; +} + +/** + * Props for form template Description component + */ +export interface FormTemplateDescriptionProps { + children: ReactNode; +} + +/** + * Props for form template FormError component + */ +export interface FormTemplateFormErrorProps { + formError?: string | { title: string; description?: string; [key: string]: any }; + alertProps?: any; +} + +/** + * Navigation schema item for wizard + */ +export interface WizardNavSchemaItem { + name: string | number; + index: number; + title?: ReactNode | string; + substepOf?: string; + substepOfTitle?: string; + primary?: boolean; +} + +/** + * Props for WizardToggle component + */ +export interface WizardToggleProps { + activeStepIndex: number; + currentStep: { + name: string; + title?: string; + }; + navSchema: WizardNavSchemaItem[]; + isOpen: boolean; + dispatch: Dispatch<{ type: 'openNav' | 'closeNav' | 'finishLoading' | 'setContainer' }>; +} + +export function isPFNavSchema(navSchema: any): navSchema is WizardNavSchemaItem[] { + return ( + Array.isArray(navSchema) && + navSchema.every( + (item) => + typeof item === 'object' && + item !== null && + (typeof item.name === 'string' || typeof item.name === 'number') && + typeof item.index === 'number' + ) + ); +} diff --git a/packages/pf4-component-mapper/src/wizard/index.js b/packages/pf4-component-mapper/src/wizard/index.js deleted file mode 100644 index 06ceeafc1..000000000 --- a/packages/pf4-component-mapper/src/wizard/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './wizard'; -export * from './wizard'; diff --git a/packages/pf4-component-mapper/src/wizard/index.d.ts b/packages/pf4-component-mapper/src/wizard/index.tsx similarity index 58% rename from packages/pf4-component-mapper/src/wizard/index.d.ts rename to packages/pf4-component-mapper/src/wizard/index.tsx index 06ceeafc1..ff90c1a6d 100644 --- a/packages/pf4-component-mapper/src/wizard/index.d.ts +++ b/packages/pf4-component-mapper/src/wizard/index.tsx @@ -1,2 +1 @@ export { default } from './wizard'; -export * from './wizard'; diff --git a/packages/pf4-component-mapper/src/wizard/wizard-components/reducer.js b/packages/pf4-component-mapper/src/wizard/wizard-components/reducer.tsx similarity index 62% rename from packages/pf4-component-mapper/src/wizard/wizard-components/reducer.js rename to packages/pf4-component-mapper/src/wizard/wizard-components/reducer.tsx index d5b3a5d11..4fa054c7b 100644 --- a/packages/pf4-component-mapper/src/wizard/wizard-components/reducer.js +++ b/packages/pf4-component-mapper/src/wizard/wizard-components/reducer.tsx @@ -1,4 +1,14 @@ -const reducer = (state, { type }) => { +interface WizardState { + loading: boolean; + container?: HTMLDivElement; + openNav: boolean; +} + +interface WizardAction { + type: 'openNav' | 'closeNav' | 'finishLoading' | 'setContainer'; +} + +const reducer = (state: WizardState, { type }: WizardAction): WizardState => { switch (type) { case 'openNav': return { diff --git a/packages/pf4-component-mapper/src/wizard/wizard-components/step-buttons.js b/packages/pf4-component-mapper/src/wizard/wizard-components/step-buttons.tsx similarity index 73% rename from packages/pf4-component-mapper/src/wizard/wizard-components/step-buttons.js rename to packages/pf4-component-mapper/src/wizard/wizard-components/step-buttons.tsx index 2a1d030cd..03cf9c142 100644 --- a/packages/pf4-component-mapper/src/wizard/wizard-components/step-buttons.js +++ b/packages/pf4-component-mapper/src/wizard/wizard-components/step-buttons.tsx @@ -3,7 +3,34 @@ import { Button, ActionList, ActionListGroup, ActionListItem } from '@patternfly import selectNext from '@data-driven-forms/common/wizard/select-next'; import { FormSpy } from '@data-driven-forms/react-form-renderer'; -const NextButton = ({ nextStep, valid, handleNext, nextLabel, getState, handleSubmit, submitLabel, conditionalSubmitFlag }) => { +interface ButtonLabels { + cancel: string; + submit: string; + back: string; + next: string; +} + +interface NextButtonProps { + nextStep?: any; + valid?: boolean; + handleNext: (nextStep?: any) => void; + nextLabel: string; + getState: () => any; + handleSubmit: () => void; + submitLabel: string; + conditionalSubmitFlag?: any; +} + +const NextButton: React.FC<NextButtonProps> = ({ + nextStep, + valid, + handleNext, + nextLabel, + getState, + handleSubmit, + submitLabel, + conditionalSubmitFlag, +}) => { const nextResult = nextStep ? selectNext(nextStep, getState) : nextStep; const progressNext = nextResult !== conditionalSubmitFlag && nextStep; return ( @@ -18,7 +45,20 @@ const NextButton = ({ nextStep, valid, handleNext, nextLabel, getState, handleSu ); }; -const WizardStepButtons = ({ +interface WizardStepButtonsProps { + buttons?: React.ComponentType<any>; + disableBack?: boolean; + handlePrev?: () => void; + nextStep?: any; + handleNext?: (nextStep?: any) => void; + buttonsClassName?: string; + buttonLabels?: ButtonLabels; + formOptions: any; + conditionalSubmitFlag?: any; + [key: string]: any; +} + +const WizardStepButtons: React.FC<WizardStepButtonsProps> = ({ buttons: Buttons, disableBack, handlePrev, @@ -39,7 +79,7 @@ const WizardStepButtons = ({ handleNext={handleNext} buttonsClassName={buttonsClassName} buttonLabels={{ cancel, submit, back, next }} - renderNextButton={(args) => ( + renderNextButton={(args: any) => ( <NextButton {...formOptions} handleNext={handleNext} nextStep={nextStep} nextLabel={next} submitLabel={submit} {...args} /> )} selectNext={selectNext} diff --git a/packages/pf4-component-mapper/src/wizard/wizard-components/wizard-nav.js b/packages/pf4-component-mapper/src/wizard/wizard-components/wizard-nav.tsx similarity index 71% rename from packages/pf4-component-mapper/src/wizard/wizard-components/wizard-nav.js rename to packages/pf4-component-mapper/src/wizard/wizard-components/wizard-nav.tsx index 9a4bdd981..4fdfa2c1c 100644 --- a/packages/pf4-component-mapper/src/wizard/wizard-components/wizard-nav.js +++ b/packages/pf4-component-mapper/src/wizard/wizard-components/wizard-nav.tsx @@ -5,10 +5,10 @@ import { WizardNav, WizardNavItem } from '@patternfly/react-core'; import isEqual from 'lodash/isEqual'; import get from 'lodash/get'; -const memoValues = (initialValue) => { +const memoValues = (initialValue: any) => { let valueCache = initialValue; - return (value) => { + return (value: any) => { if (!isEqual(value, valueCache)) { valueCache = value; return true; @@ -18,7 +18,24 @@ const memoValues = (initialValue) => { }; }; -const WizardNavigationInternal = React.memo( +interface WizardNavigationInternalProps { + navSchema: Array<{ + primary?: boolean; + isProgressAfterSubmissionStep?: boolean; + substepOf?: string; + substepOfTitle?: string; + name: string; + title: string; + index: number; + }>; + activeStepIndex: number; + maxStepIndex: number; + jumpToStep: (index: number, valid: boolean) => void; + valid: boolean; + validating: boolean; +} + +const WizardNavigationInternal = React.memo<WizardNavigationInternalProps>( ({ navSchema, activeStepIndex, maxStepIndex, jumpToStep, valid, validating }) => ( <Fragment> {navSchema @@ -34,27 +51,27 @@ const WizardNavigationInternal = React.memo( content={step.substepOfTitle || step.title} isCurrent={substeps ? activeStepIndex >= step.index && activeStepIndex < step.index + substeps.length : activeStepIndex === step.index} isDisabled={isValid ? maxStepIndex < step.index : step.index > activeStepIndex} - onClick={(e) => { + onClick={(e: React.MouseEvent) => { e.preventDefault(); jumpToStep(step.index, isValid); }} step={step.index} - type="button" + stepIndex={step.index} > {substeps && ( <WizardNav isInnerList> {substeps.map((substep) => ( <WizardNavItem - type="button" key={substep.name} content={substep.title} isCurrent={activeStepIndex === substep.index} isDisabled={isValid ? maxStepIndex < substep.index : substep.index > activeStepIndex} - onClick={(e) => { + onClick={(e: React.MouseEvent) => { e.preventDefault(); jumpToStep(substep.index, isValid); }} step={substep.index} + stepIndex={substep.index} /> ))} </WizardNav> @@ -67,7 +84,16 @@ const WizardNavigationInternal = React.memo( isEqual ); -const WizardNavigation = ({ setPrevSteps, crossroads, values, ...props }) => { +WizardNavigationInternal.displayName = 'WizardNavigationInternal'; + +interface WizardNavigationProps extends WizardNavigationInternalProps { + setPrevSteps: () => void; + crossroads?: string[]; + values: any; + isDynamic?: boolean; +} + +const WizardNavigation: React.FC<WizardNavigationProps> = ({ setPrevSteps, crossroads, values, ...props }) => { const [memoValue] = useState(() => memoValues( crossroads diff --git a/packages/pf4-component-mapper/src/wizard/wizard-components/wizard-step.js b/packages/pf4-component-mapper/src/wizard/wizard-components/wizard-step.tsx similarity index 57% rename from packages/pf4-component-mapper/src/wizard/wizard-components/wizard-step.js rename to packages/pf4-component-mapper/src/wizard/wizard-components/wizard-step.tsx index b298fee81..cc6d2f8d5 100644 --- a/packages/pf4-component-mapper/src/wizard/wizard-components/wizard-step.js +++ b/packages/pf4-component-mapper/src/wizard/wizard-components/wizard-step.tsx @@ -1,8 +1,13 @@ -import React, { Fragment, useEffect, useRef } from 'react'; +import React, { Fragment, useEffect, useRef, ReactNode } from 'react'; import { Title, WizardBody } from '@patternfly/react-core'; import WizardStepButtons from './step-buttons'; -export const RenderTitle = ({ title, customTitle }) => +interface RenderTitleProps { + title: string; + customTitle?: ReactNode; +} + +export const RenderTitle: React.FC<RenderTitleProps> = ({ title, customTitle }) => customTitle ? ( customTitle ) : ( @@ -11,18 +16,42 @@ export const RenderTitle = ({ title, customTitle }) => ); -const DefaultStepTemplate = ({ formFields, formRef, title, customTitle, showTitle, showTitles }) => ( +interface DefaultStepTemplateProps { + formFields: ReactNode[]; + formRef: React.RefObject; + title: string; + customTitle?: ReactNode; + showTitle?: boolean; + showTitles?: boolean; + [key: string]: any; +} + +const DefaultStepTemplate: React.FC = ({ formFields, formRef, title, customTitle, showTitle, showTitles }) => (
{((showTitles && showTitle !== false) || showTitle) && } {formFields}
); -const WizardStep = ({ +interface WizardStepProps { + name: string; + title?: string; + description?: string; + fields?: any[]; + formOptions: any; + showTitles?: boolean; + showTitle?: boolean; + customTitle?: ReactNode; + hasNoBodyPadding?: boolean; + StepTemplate?: React.ComponentType; + [key: string]: any; +} + +const WizardStep: React.FC = ({ name, title, description, - fields, + fields = [], formOptions, showTitles, showTitle, @@ -31,13 +60,13 @@ const WizardStep = ({ StepTemplate = DefaultStepTemplate, ...rest }) => { - const formRef = useRef(); + const formRef = useRef(null); useEffect(() => { // HACK: I can not pass ref to WizardBody because it is not // wrapped by forwardRef. However, the step body (the one that overflows) // is the grand parent of the form element. - const stepBody = formRef.current && formRef.current.parentNode.parentNode; + const stepBody = formRef.current && (formRef.current.parentNode?.parentNode as HTMLElement); stepBody && stepBody.scrollTo({ top: 0, left: 0, behavior: 'smooth' }); }, [name]); @@ -45,7 +74,7 @@ const WizardStep = ({ formOptions.renderForm([item], formOptions))} + formFields={fields.map((item) => formOptions.renderForm([item]))} name={name} title={title} description={description} diff --git a/packages/pf4-component-mapper/src/wizard/wizard-components/wizard-toggle.js b/packages/pf4-component-mapper/src/wizard/wizard-components/wizard-toggle.tsx similarity index 80% rename from packages/pf4-component-mapper/src/wizard/wizard-components/wizard-toggle.js rename to packages/pf4-component-mapper/src/wizard/wizard-components/wizard-toggle.tsx index 118ca3df9..d028b1781 100644 --- a/packages/pf4-component-mapper/src/wizard/wizard-components/wizard-toggle.js +++ b/packages/pf4-component-mapper/src/wizard/wizard-components/wizard-toggle.tsx @@ -1,7 +1,8 @@ import React from 'react'; import { AngleRightIcon, CaretDownIcon } from '@patternfly/react-icons'; +import { WizardToggleProps } from '../../types'; -const WizardToggle = ({ activeStepIndex, currentStep, navSchema, isOpen, dispatch }) => { +const WizardToggle: React.FC = ({ activeStepIndex, currentStep, navSchema, isOpen, dispatch }) => { const substepTitle = navSchema.find((step) => step.name === currentStep.name)?.substepOfTitle; const substepName = navSchema.find((step) => step.name === currentStep.name)?.substepOf; @@ -20,7 +21,7 @@ const WizardToggle = ({ activeStepIndex, currentStep, navSchema, isOpen, dispatc >
  1. - {index + 1} {activeStepName} + {index && typeof index === 'number' ? index + 1 : activeStepIndex + 1} {activeStepName} {activeStepSubName &&
  2. {activeStepSubName &&
  3. {activeStepSubName}
  4. } diff --git a/packages/pf4-component-mapper/src/wizard/wizard.d.ts b/packages/pf4-component-mapper/src/wizard/wizard.d.ts deleted file mode 100644 index 8c53facfc..000000000 --- a/packages/pf4-component-mapper/src/wizard/wizard.d.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { ReactNode } from "react"; -import { Field, AnyObject } from "@data-driven-forms/react-form-renderer"; -import { ModalProps } from "@patternfly/react-core"; - -export interface WizardButtonLabels { - submit?: ReactNode; - cancel?: ReactNode; - back?: ReactNode; - next?: ReactNode; -} - -export interface WizardNextStepFunctionArgument { - values?: AnyObject; - value?: any; -} - -export interface WizardNextStepFunction { - (formState: WizardNextStepFunctionArgument): string; -} - -export interface WizardStepMapper { - [key: string]: string | number; -} - -export interface WizardNextStepMapper { - when: string; - stepMapper: WizardStepMapper; -} - -export type WizardNextStep = string | WizardNextStepMapper | WizardNextStepFunction; - -export interface SelectNextFunction { - (nextStep: WizardNextStep, getState: Function): string; -} - -export interface HandleNextFunction { - (nextStep: string): void; -} - -export interface WizardButtonsProps { - disableBack?: boolean; - handlePrev: Function; - nextStep?: WizardNextStep; - handleNext: HandleNextFunction; - buttonsClassname?: string; - buttonLabels: WizardButtonLabels; - renderNextButton: Function; - selectNext: SelectNextFunction; -} - -export interface SubstepOfObject { - name: string; - title?: ReactNode; -} - -export interface StepTemplateProps { - title?: ReactNode; - formFields: ReactNode; - showTitles?: boolean; - showTitle?: boolean; - customTitle?: ReactNode; - fields: Field[]; -} - -export interface WizardField { - name: string | number; - fields: Field[]; - nextStep?: WizardNextStep; - substepOf?: string | number | SubstepOfObject; - title?: ReactNode; - showTitle?: boolean; - customTitle?: ReactNode; - disableForwardJumping?: boolean; - buttons?: ReactNode | React.ComponentType; - StepTemplate?: React.ComponentType; - hasNoBodyPadding?: boolean; -} - -export interface WizardProps { - buttonLabels?: WizardButtonLabels; - buttonsClassName?: string; - title?: ReactNode; - description?: ReactNode; - inModal?: boolean; - isDynamic?: boolean; - showTitles?: boolean; - crossroads?: string[]; - fields: WizardField[]; - hideClose?: boolean; - titleId?: string; - descriptionId?: string; - closeButtonAriaLabel?: string; - hasNoBodyPadding?: boolean; - navAriaLabel?: string; - StepTemplate?: React.ComponentType; - ModalProps?: ModalProps -} - -declare const Wizard: React.ComponentType; - -export default Wizard; diff --git a/packages/pf4-component-mapper/src/wizard/wizard.js b/packages/pf4-component-mapper/src/wizard/wizard.tsx similarity index 68% rename from packages/pf4-component-mapper/src/wizard/wizard.js rename to packages/pf4-component-mapper/src/wizard/wizard.tsx index 7ce588484..a004e5e59 100644 --- a/packages/pf4-component-mapper/src/wizard/wizard.js +++ b/packages/pf4-component-mapper/src/wizard/wizard.tsx @@ -1,8 +1,8 @@ -import React, { useReducer, useEffect, useContext } from 'react'; -import { FormSpy, WizardContext } from '@data-driven-forms/react-form-renderer'; +import React, { useReducer, useEffect, useContext, ReactNode, useMemo } from 'react'; +import { Field, FormSpy, WizardContext } from '@data-driven-forms/react-form-renderer'; import Wizard from '@data-driven-forms/common/wizard/wizard'; -import { WizardNav, WizardHeader } from '@patternfly/react-core'; +import { WizardNav, WizardHeader, WizardProps } from '@patternfly/react-core'; import { Modal as PF4Modal } from '@patternfly/react-core/deprecated'; import WizardStep from './wizard-components/wizard-step'; @@ -11,8 +11,16 @@ import './wizard-components/wizard-styles.css'; import WizardNavigation from './wizard-components/wizard-nav'; import reducer from './wizard-components/reducer'; import WizardToggle from './wizard-components/wizard-toggle'; +import { isPFNavSchema } from '../types'; -const Modal = ({ children, container, inModal, ...rest }) => +interface ModalProps { + children: ReactNode; + container?: HTMLDivElement; + inModal?: boolean; + [key: string]: any; +} + +const Modal: React.FC = ({ children, container, inModal, ...rest }) => inModal ? ( {children} @@ -21,7 +29,28 @@ const Modal = ({ children, container, inModal, ...rest }) => children ); -const WizardInternal = ({ +interface WizardInternalProps { + inModal?: boolean; + title?: string; + description?: string; + buttonLabels?: any; + buttonsClassName?: string; + showTitles?: boolean; + container?: HTMLDivElement; + hideClose?: boolean; + titleId?: string; + descriptionId?: string; + closeButtonAriaLabel?: string; + hasNoBodyPadding?: boolean; + navAriaLabel?: string; + StepTemplate?: React.ComponentType; + className?: string; + conditionalSubmitFlag?: any; + ModalProps?: any; + [key: string]: any; +} + +const WizardInternal: React.FC = ({ inModal, title, description, @@ -81,11 +110,19 @@ const WizardInternal = ({ }; }, [state.container, inModal]); + const isValidNavSchema = useMemo(() => isPFNavSchema(navSchema), [navSchema]); + + if (!isValidNavSchema) { + throw new Error( + 'Invalid navigation schema provided to PF Wizard. Please ensure navSchema is an array of objects with name, index, and title properties.' + ); + } + if (state.loading) { return null; } - const isProgressAfterSubmissionStep = currentStep.isProgressAfterSubmissionStep; + const isProgressAfterSubmissionStep = currentStep?.isProgressAfterSubmissionStep; return (
    { + onKeyDown={(e: React.KeyboardEvent) => { onKeyDown(e); if (e.key === 'Escape' && inModal) { - formOptions.onCancel(); + formOptions.onCancel(formOptions.getState().values); } }} {...rest} @@ -120,13 +157,14 @@ const WizardInternal = ({ /> )} {isProgressAfterSubmissionStep ? ( - currentStep.fields.map((item) => formOptions.renderForm([item], formOptions)) + currentStep?.fields.map((item: any) => formOptions.renderForm([item])) ) : ( <> @@ -136,11 +174,12 @@ const WizardInternal = ({ {({ values, valid, validating }) => ( { + jumpToStep={(...args: any[]) => { state.openNav && dispatch({ type: 'closeNav' }); return jumpToStep(...args); }} @@ -162,7 +201,7 @@ const WizardInternal = ({ StepTemplate={StepTemplate} {...currentStep} formOptions={formOptions} - handleNext={(nextStep) => handleNext(nextStep)} + handleNext={(nextStep: any) => handleNext(nextStep)} handlePrev={handlePrev} disableBack={activeStepIndex === 0} /> @@ -182,7 +221,13 @@ const defaultLabels = { next: 'Next', }; -const WizardFunction = ({ buttonLabels = {}, ...props }) => ( +interface WizardFunctionProps extends WizardProps { + buttonLabels?: any; + fields: Field[]; + [key: string]: any; +} + +const WizardFunction: React.FC = ({ buttonLabels = {}, ...props }) => ( ); diff --git a/packages/pf4-component-mapper/tsconfig.cjs.json b/packages/pf4-component-mapper/tsconfig.cjs.json new file mode 100644 index 000000000..4210c64ee --- /dev/null +++ b/packages/pf4-component-mapper/tsconfig.cjs.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "./", + "declaration": true, + "emitDeclarationOnly": false + } +} \ No newline at end of file diff --git a/packages/pf4-component-mapper/tsconfig.demo.json b/packages/pf4-component-mapper/tsconfig.demo.json new file mode 100644 index 000000000..f37217b80 --- /dev/null +++ b/packages/pf4-component-mapper/tsconfig.demo.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "sourceMap": true, + "module": "esnext", + "moduleResolution": "node", + "target": "es5", + "lib": ["es6", "dom"], + "jsx": "react-jsx", + "allowSyntheticDefaultImports": true, + "noErrorTruncation": true, + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "strict": false, + "noEmit": false, + "isolatedModules": false, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "noImplicitAny": false, + "noImplicitReturns": false, + "noImplicitThis": false, + "rootDir": "." + }, + "include": [ + "./demo/**/*", + "./src/**/*" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/packages/pf4-component-mapper/tsconfig.esm.json b/packages/pf4-component-mapper/tsconfig.esm.json new file mode 100644 index 000000000..97b6f3a3b --- /dev/null +++ b/packages/pf4-component-mapper/tsconfig.esm.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "esnext", + "outDir": "./esm", + "declaration": true, + "emitDeclarationOnly": false + } +} \ No newline at end of file diff --git a/packages/pf4-component-mapper/tsconfig.json b/packages/pf4-component-mapper/tsconfig.json index da332a59e..c8e6f097b 100644 --- a/packages/pf4-component-mapper/tsconfig.json +++ b/packages/pf4-component-mapper/tsconfig.json @@ -1,17 +1,25 @@ { "compilerOptions": { "sourceMap": true, - "module": "es6", + "module": "esnext", "moduleResolution": "node", "target": "es5", - "lib": ["es2020", "dom"], - "jsx": "preserve", + "lib": ["es6", "dom"], + "jsx": "react-jsx", "allowSyntheticDefaultImports": true, "noErrorTruncation": true, - "allowJs": true, - "strict": true, - "noEmit": true + "allowJs": false, + "strict": false, + "declaration": true, + "emitDeclarationOnly": false, + "outDir": "./dist", + "rootDir": "./src", + "skipLibCheck": true, + "esModuleInterop": true, + "noImplicitAny": false, + "noImplicitReturns": false, + "noImplicitThis": false }, - "include": ["./src/**/*.ts", "./src/**/*.tsx", "./src/**/*.js"], - "exclude": ["./dist"] + "include": ["./src/**/*.ts", "./src/**/*.tsx"], + "exclude": ["./dist", "./src/tests/**/*", "./node_modules", "../*/node_modules", "../../node_modules"] } \ No newline at end of file diff --git a/packages/pf4-component-mapper/tsconfig.spec.json b/packages/pf4-component-mapper/tsconfig.spec.json index 0423d69a7..96d66c130 100644 --- a/packages/pf4-component-mapper/tsconfig.spec.json +++ b/packages/pf4-component-mapper/tsconfig.spec.json @@ -12,6 +12,8 @@ }, "include": [ "jest.config.ts", + "src/**/*.ts", + "src/**/*.tsx", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.test.tsx",