From f3d18c82c796bf7753c8df7c70831895d3cbd0db Mon Sep 17 00:00:00 2001 From: Andrey Yamanov Date: Thu, 21 Nov 2024 11:36:31 +0100 Subject: [PATCH 1/6] fix(Form): optimize field creation --- .changeset/quick-badgers-guess.md | 5 ++++ .../TextInputMapper.stories.tsx | 19 ++++++++++++++ src/components/form/Form/use-form.tsx | 25 ++++++++++--------- 3 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 .changeset/quick-badgers-guess.md diff --git a/.changeset/quick-badgers-guess.md b/.changeset/quick-badgers-guess.md new file mode 100644 index 00000000..b5deb14e --- /dev/null +++ b/.changeset/quick-badgers-guess.md @@ -0,0 +1,5 @@ +--- +'@cube-dev/ui-kit': minor +--- + +Do not create field instance for non-exist fields in Form. Use default values from Form when creating new fields. diff --git a/src/components/fields/TextInputMapper/TextInputMapper.stories.tsx b/src/components/fields/TextInputMapper/TextInputMapper.stories.tsx index fa2d4067..874774fd 100644 --- a/src/components/fields/TextInputMapper/TextInputMapper.stories.tsx +++ b/src/components/fields/TextInputMapper/TextInputMapper.stories.tsx @@ -28,6 +28,22 @@ const Template: StoryFn = ({ ...props }) => ( ); const FormTemplate: StoryFn = ({ ...props }) => ( +
console.log('! onSubmit', data)} + > + console.log('! onChange', value)} + /> + Submit + +); + +const FormTemplateSync: StoryFn = ({ ...props }) => (
{ export const WithinForm = FormTemplate.bind({}); WithinForm.args = {}; + +export const WithinFormInputSync = FormTemplateSync.bind({}); +WithinFormInputSync.args = {}; diff --git a/src/components/form/Form/use-form.tsx b/src/components/form/Form/use-form.tsx index aa5519be..8f11fd95 100644 --- a/src/components/form/Form/use-form.tsx +++ b/src/components/form/Form/use-form.tsx @@ -34,7 +34,7 @@ export class CubeFormInstance< TFormData extends CubeFormData = CubeFormData, > { public forceReRender: () => void = () => {}; - private initialFields: PartialString = {}; + private defaultValues: PartialString = {}; private fields: TFormData = {} as TFormData; public ref = {}; public isSubmitting = false; @@ -70,17 +70,17 @@ export class CubeFormInstance< newData: PartialString, touched?: boolean, skipRender?: boolean, - createFields = false, inputOnly = false, ) => { let flag = false; + newData = { ...newData, ...dotize.convert(newData) }; + Object.keys(newData).forEach((name: keyof T & string) => { let field = this.fields[name]; - if (!field && createFields) { - this.createField(name, skipRender); - field = this.fields[name]; + if (!field) { + return; } if (!field || isEqual(field.value, newData[name])) { @@ -205,12 +205,12 @@ export class CubeFormInstance< } setInitialFieldsValue(values: PartialString): void { - this.initialFields = { ...values, ...dotize.convert(values) }; + this.defaultValues = { ...values, ...dotize.convert(values) }; } updateInitialFieldsValue(values: FieldTypes): void { - this.initialFields = { - ...this.initialFields, + this.defaultValues = { + ...this.defaultValues, ...values, ...dotize.convert(values), }; @@ -218,14 +218,14 @@ export class CubeFormInstance< resetFields(names?: (keyof T & string)[], skipRender?: boolean): void { const fieldsValue = this.getFieldsValue(); - const fieldNames = Object.keys({ ...fieldsValue, ...this.initialFields }); + const fieldNames = Object.keys({ ...fieldsValue, ...this.defaultValues }); const filteredFieldNames = names ? fieldNames.filter((name) => names.includes(name)) : fieldNames; const values = filteredFieldNames.reduce((map, name) => { - if (name in this.initialFields) { - map[name] = this.initialFields[name]; + if (name in this.defaultValues) { + map[name] = this.defaultValues[name]; } else { map[name] = undefined; } @@ -233,7 +233,7 @@ export class CubeFormInstance< return map; }, {}); - this.setFieldsValue(values, false, skipRender, true); + this.setFieldsValue(values, false, skipRender); } async validateField(name: Name): Promise { @@ -404,6 +404,7 @@ export class CubeFormInstance< touched: false, errors: [], validationId: 0, + value: this.defaultValues[name], ...data, // it should be impossible to define or override status value status: data?.errors?.length ? 'invalid' : undefined, From 95601dc7c30247b9cb841be38112504e6c20068f Mon Sep 17 00:00:00 2001 From: Andrey Yamanov Date: Thu, 21 Nov 2024 14:51:00 +0100 Subject: [PATCH 2/6] fix(TextInputMapper): add keyboard interaction --- .../TextInputMapper/TextInputMapper.tsx | 48 ++++++++++++++++--- src/components/layout/Flex.tsx | 4 +- src/components/layout/Flow.tsx | 4 +- src/components/layout/Grid.tsx | 4 +- src/components/layout/Space.tsx | 4 +- src/tasty/types.ts | 6 +-- 6 files changed, 53 insertions(+), 17 deletions(-) diff --git a/src/components/fields/TextInputMapper/TextInputMapper.tsx b/src/components/fields/TextInputMapper/TextInputMapper.tsx index da6bb1c1..18231e6f 100644 --- a/src/components/fields/TextInputMapper/TextInputMapper.tsx +++ b/src/components/fields/TextInputMapper/TextInputMapper.tsx @@ -1,6 +1,8 @@ import { ComponentType, + ForwardedRef, forwardRef, + KeyboardEvent, useEffect, useMemo, useRef, @@ -9,7 +11,7 @@ import { import { useEvent } from '../../../_internal/hooks'; import { FieldBaseProps } from '../../../shared'; -import { mergeProps } from '../../../utils/react/index'; +import { mergeProps, useCombinedRefs } from '../../../utils/react/index'; import { useFieldProps, useFormProps, wrapWithField } from '../../form'; import { CloseIcon, PlusIcon } from '../../../icons'; import { Button } from '../../actions'; @@ -63,7 +65,12 @@ function removeDuplicates(mappings: Mapping[]) { }); } -function TextInputMapper(props: CubeTextInputMapperProps, ref: any) { +function TextInputMapper( + props: CubeTextInputMapperProps, + ref: ForwardedRef, +) { + ref = useCombinedRefs(ref); + props = useFormProps(props); props = useFieldProps(props, { defaultValidationTrigger: 'onChange', @@ -122,7 +129,7 @@ function TextInputMapper(props: CubeTextInputMapperProps, ref: any) { const onMappingsChange = useEvent((newMappings: Mapping[]) => { const newValue = newMappings.reduce( (acc, { key, value }) => { - acc[key] = value; + acc[key.trim()] = value.trim(); return acc; }, @@ -136,8 +143,25 @@ function TextInputMapper(props: CubeTextInputMapperProps, ref: any) { } else { onChange?.(newValue); } + + const updatedMappings = extractLocalValues(newValue ?? {}, newMappings); + + if (JSON.stringify(updatedMappings) !== JSON.stringify(mappings)) { + setMappings(updatedMappings); + } }); + // useEffect(() => { + // // focus on the last non-disabled input + // setTimeout(() => { + // ( + // ref?.current?.querySelector( + // '[data-qa="Mapping"]:last-child input:not([disabled])', + // ) as HTMLInputElement + // )?.focus(); + // }, 100); + // }, [mappings.length]); + const addNewMapping = useEvent(() => { setMappings((prev) => { return [...prev, { key: '', value: '', id: counterRef.current++ }]; @@ -176,17 +200,26 @@ function TextInputMapper(props: CubeTextInputMapperProps, ref: any) { onMappingsChange(mappings); }); + const onKeyDown = useEvent((e: KeyboardEvent) => { + // if Ctrl+Enter or Cmd+Enter is pressed then add new mapping if that's enabled + if ((e.ctrlKey || e.metaKey) && e.key === 'Enter' && showNewButton) { + addNewMapping(); + } + }); + const renderedMappings = useMemo(() => { - return mappings.map((mapping) => { + return mappings.map((mapping, index) => { const { key, value, id } = mapping; return ( - {[...renderedMappings]} + + + {[...renderedMappings]} + {showNewButton ? ( {/** Hotfix for inconsistent alignment with the label **/} @@ -251,6 +286,7 @@ export interface CubeTextInputMapperInputProps { onChange?: (id: number, newValue: string) => void; onSubmit?: (id: number) => void; isDisabled?: boolean; + autoFocus?: boolean; } function TextInputMapperInput(props: CubeTextInputMapperInputProps) { diff --git a/src/components/layout/Flex.tsx b/src/components/layout/Flex.tsx index 8cbfd37f..445b2038 100644 --- a/src/components/layout/Flex.tsx +++ b/src/components/layout/Flex.tsx @@ -1,7 +1,7 @@ import { forwardRef } from 'react'; import { - BaseProps, + AllBaseProps, CONTAINER_STYLES, ContainerStyleProps, extractStyles, @@ -16,7 +16,7 @@ const FlexElement = tasty({ }, }); -export interface CubeFlexProps extends BaseProps, ContainerStyleProps {} +export interface CubeFlexProps extends AllBaseProps, ContainerStyleProps {} export const Flex = forwardRef(function Flex(props: CubeFlexProps, ref) { const styles = extractStyles(props, CONTAINER_STYLES); diff --git a/src/components/layout/Flow.tsx b/src/components/layout/Flow.tsx index d0b7ce25..a4bcaec8 100644 --- a/src/components/layout/Flow.tsx +++ b/src/components/layout/Flow.tsx @@ -1,7 +1,7 @@ import { forwardRef } from 'react'; import { - BaseProps, + AllBaseProps, CONTAINER_STYLES, ContainerStyleProps, extractStyles, @@ -18,7 +18,7 @@ const FlowElement = tasty({ const STYLE_PROPS = CONTAINER_STYLES; -export interface CubeFlowProps extends BaseProps, ContainerStyleProps {} +export interface CubeFlowProps extends AllBaseProps, ContainerStyleProps {} export const Flow = forwardRef(function Flow(props: CubeFlowProps, ref) { const styles = extractStyles(props, STYLE_PROPS); diff --git a/src/components/layout/Grid.tsx b/src/components/layout/Grid.tsx index 16d3f911..220276cf 100644 --- a/src/components/layout/Grid.tsx +++ b/src/components/layout/Grid.tsx @@ -1,7 +1,7 @@ import { forwardRef } from 'react'; import { - BaseProps, + AllBaseProps, CONTAINER_STYLES, ContainerStyleProps, extractStyles, @@ -18,7 +18,7 @@ const GridElement = tasty({ }); export interface CubeGridProps - extends BaseProps, + extends Omit, ContainerStyleProps, ShortGridStyles {} diff --git a/src/components/layout/Space.tsx b/src/components/layout/Space.tsx index fbb3f68b..219c4d4f 100644 --- a/src/components/layout/Space.tsx +++ b/src/components/layout/Space.tsx @@ -1,7 +1,7 @@ import { forwardRef } from 'react'; import { - BaseProps, + AllBaseProps, CONTAINER_STYLES, ContainerStyleProps, extractStyles, @@ -24,7 +24,7 @@ const SpaceElement = tasty({ }, }); -export interface CubeSpaceProps extends BaseProps, ContainerStyleProps { +export interface CubeSpaceProps extends AllBaseProps, ContainerStyleProps { direction?: 'vertical' | 'horizontal'; } diff --git a/src/tasty/types.ts b/src/tasty/types.ts index 73ff3c7b..536f2d58 100644 --- a/src/tasty/types.ts +++ b/src/tasty/types.ts @@ -80,10 +80,10 @@ export interface BasePropsWithoutChildren theme?: 'default' | 'danger' | 'special' | (string & {}); } -export interface BaseProps +export interface BaseProps extends AriaLabelingProps, BasePropsWithoutChildren, - Pick, 'children'> {} + Pick, 'children'> {} export interface AllBaseProps extends BaseProps, @@ -98,7 +98,7 @@ export interface AllBaseProps | 'height' | 'width' > { - as?: string; + as?: K; } export type BaseStyleProps = Pick; From e0ab527c9e56fff06e64023b9a617262f4acaa45 Mon Sep 17 00:00:00 2001 From: Andrey Yamanov Date: Thu, 21 Nov 2024 14:51:41 +0100 Subject: [PATCH 3/6] chore: add changeset --- .changeset/brave-brooms-smile.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/brave-brooms-smile.md diff --git a/.changeset/brave-brooms-smile.md b/.changeset/brave-brooms-smile.md new file mode 100644 index 00000000..81054f7a --- /dev/null +++ b/.changeset/brave-brooms-smile.md @@ -0,0 +1,5 @@ +--- +'@cube-dev/ui-kit': patch +--- + +Add input trimming and keyboard interaction for TextInputMapper. From a028035968d09c0b665b126debac5d0b241ffc78 Mon Sep 17 00:00:00 2001 From: Andrey Yamanov Date: Thu, 21 Nov 2024 14:52:42 +0100 Subject: [PATCH 4/6] feat: add support for all html attributes in basic components --- .changeset/lovely-carrots-hammer.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/lovely-carrots-hammer.md diff --git a/.changeset/lovely-carrots-hammer.md b/.changeset/lovely-carrots-hammer.md new file mode 100644 index 00000000..905746bd --- /dev/null +++ b/.changeset/lovely-carrots-hammer.md @@ -0,0 +1,5 @@ +--- +'@cube-dev/ui-kit': patch +--- + +Add support for all html attributes in basic components. From 8bb0cdbdf0710a71893eb4a9accd60885a14ead7 Mon Sep 17 00:00:00 2001 From: Andrey Yamanov Date: Thu, 21 Nov 2024 14:55:42 +0100 Subject: [PATCH 5/6] feat: add support for all html attributes in basic components * 2 --- src/tasty/types.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tasty/types.ts b/src/tasty/types.ts index 536f2d58..8f9131ee 100644 --- a/src/tasty/types.ts +++ b/src/tasty/types.ts @@ -97,6 +97,8 @@ export interface AllBaseProps | 'color' | 'height' | 'width' + | 'content' + | 'translate' > { as?: K; } From 9dfa070ea3a0c92fca8f5d15a7800ee013bee189 Mon Sep 17 00:00:00 2001 From: Andrey Yamanov Date: Thu, 21 Nov 2024 15:00:59 +0100 Subject: [PATCH 6/6] fix(TextInputMapper): layout --- src/components/fields/TextInputMapper/TextInputMapper.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/fields/TextInputMapper/TextInputMapper.tsx b/src/components/fields/TextInputMapper/TextInputMapper.tsx index 18231e6f..11fe8a66 100644 --- a/src/components/fields/TextInputMapper/TextInputMapper.tsx +++ b/src/components/fields/TextInputMapper/TextInputMapper.tsx @@ -255,10 +255,8 @@ function TextInputMapper( }, [JSON.stringify(mappings)]); const element = ( - - - {[...renderedMappings]} - + + {[...renderedMappings]} {showNewButton ? ( {/** Hotfix for inconsistent alignment with the label **/}