diff --git a/.github/workflows/pkg.pr.new.yml b/.github/workflows/pkg.pr.new.yml new file mode 100644 index 0000000..5577633 --- /dev/null +++ b/.github/workflows/pkg.pr.new.yml @@ -0,0 +1,29 @@ +name: PKG PR New +on: + workflow_dispatch: + pull_request: + types: [opened, synchronize, ready_for_review] + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - run: yarn --frozen-lockfile + + # append the git commit to the package.json version. + # We do this because some cache mechanisms (like nextjs) don't work well with the same version and ignore the changes + # until you manually delete the cache + - run: jq '.version = .version + "-" + env.GITHUB_SHA' package.json > package.json.tmp && mv package.json.tmp package.json + + - run: npx pkg-pr-new publish \ No newline at end of file diff --git a/src/lib/AsyncTypeaheadInput.tsx b/src/lib/AsyncTypeaheadInput.tsx index 4b23909..a62f859 100644 --- a/src/lib/AsyncTypeaheadInput.tsx +++ b/src/lib/AsyncTypeaheadInput.tsx @@ -10,7 +10,7 @@ import { useFormContext } from "./context/FormContext"; import TypeheadRef from "react-bootstrap-typeahead/types/core/Typeahead"; import { LabelValueOption } from "./types/LabelValueOption"; -interface AsyncTypeaheadProps extends CommonTypeaheadProps { +type AsyncTypeaheadProps = CommonTypeaheadProps & { queryFn: (query: string) => Promise; reactBootstrapTypeaheadProps?: Partial; } @@ -38,7 +38,7 @@ const AsyncTypeaheadInput = (props: AsyncTypeaheadProps(null); const { diff --git a/src/lib/DatePickerInput.tsx b/src/lib/DatePickerInput.tsx index ad5f9f9..a64c683 100644 --- a/src/lib/DatePickerInput.tsx +++ b/src/lib/DatePickerInput.tsx @@ -14,7 +14,7 @@ interface DatePickerRenderAddonProps { toggleDatePicker: () => void; } -interface DatePickerInputProps extends Omit, "onChange" | "style"> { +type DatePickerInputProps = Omit, "onChange" | "style"> & { /** * The props for the date picker component: https://reactdatepicker.com/ */ @@ -68,7 +68,7 @@ const DatePickerInput = (props: DatePickerInputProps) hideValidationMessage = false, } = props; - const { id, name } = useSafeNameId(initialName, initialId); + const { id, name } = useSafeNameId(initialName ?? "", initialId); const { control, getValues, setValue, disabled: formDisabled } = useFormContext(); const internalDatePickerRef = useRef(); const formGroupId = useRef(guidGen()); diff --git a/src/lib/FormGroupLayout.tsx b/src/lib/FormGroupLayout.tsx index 2b05bbb..d8e1ce1 100644 --- a/src/lib/FormGroupLayout.tsx +++ b/src/lib/FormGroupLayout.tsx @@ -1,3 +1,4 @@ +/* eslint-disable complexity */ import { PropsWithChildren, ReactNode, CSSProperties, useMemo } from "react"; import { FieldError, FieldValues, get } from "react-hook-form"; import { FormGroup, FormFeedback, FormText, InputGroup } from "reactstrap"; @@ -5,21 +6,20 @@ import { useSafeNameId } from "src/lib/hooks/useSafeNameId"; import { CommonInputProps, MergedAddonProps } from "./types/CommonInputProps"; import "./styles/FormGroupLayout.css"; import { FormGroupLayoutLabel } from "./FormGroupLayoutLabel"; -import { useFormContext } from "./context/FormContext"; +import { UnknownType, useFormContextInternal } from "./context/FormContext"; -interface FormGroupLayoutProps - extends PropsWithChildren< - Pick, "helpText" | "label" | "name" | "id" | "labelToolTip" | "inputOnly" | "hideValidationMessage"> - > { +type FormGroupLayoutProps = PropsWithChildren< + Pick, "helpText" | "label" | "name" | "id" | "labelToolTip" | "inputOnly" | "hideValidationMessage"> +> & { layout?: "checkbox" | "switch"; addonLeft?: ReactNode | ((props: TRenderAddon) => ReactNode); addonRight?: ReactNode | ((props: TRenderAddon) => ReactNode); addonProps?: MergedAddonProps; inputGroupStyle?: CSSProperties; formGroupId?: string; -} +}; -const FormGroupLayout = (props: FormGroupLayoutProps) => { +const FormGroupLayout = (props: FormGroupLayoutProps) => { const { label, helpText, @@ -34,13 +34,10 @@ const FormGroupLayout = (props: F addonProps, hideValidationMessage = false, } = props; - const { name, id } = useSafeNameId(props.name, props.id); - const { - formState: { errors }, - hideValidationMessages, - } = useFormContext(); + const { name, id } = useSafeNameId(props?.name ?? "", props.id); + const { formState, hideValidationMessages = false } = useFormContextInternal() ?? {}; - const fieldError = get(errors, name) as FieldError | undefined; + const fieldError = formState ? (get(formState.errors, name) as FieldError | undefined) : undefined; const errorMessage = String(fieldError?.message); const switchLayout = layout === "switch"; diff --git a/src/lib/FormGroupLayoutLabel.tsx b/src/lib/FormGroupLayoutLabel.tsx index f0d795d..68dd20c 100644 --- a/src/lib/FormGroupLayoutLabel.tsx +++ b/src/lib/FormGroupLayoutLabel.tsx @@ -1,19 +1,20 @@ import { ReactNode } from "react"; -import { useFormContext } from "./context/FormContext"; -import { FieldPath, FieldValues } from "react-hook-form"; +import { UnknownType, useFormContextInternal } from "./context/FormContext"; +import { FieldValues, Path } from "react-hook-form"; import { Label, UncontrolledTooltip } from "reactstrap"; +import { CommonInputProps } from "./types/CommonInputProps"; -interface FormGroupLayoutLabelProps { +interface FormGroupLayoutLabelProps { label: ReactNode; tooltip?: ReactNode; - fieldName: FieldPath; + fieldName: CommonInputProps["name"]; fieldId: string; layout?: "checkbox" | "switch"; } -const FormGroupLayoutLabel = (props: FormGroupLayoutLabelProps) => { +const FormGroupLayoutLabel = (props: FormGroupLayoutLabelProps) => { const { label, tooltip, fieldName, layout, fieldId } = props; - const { requiredFields } = useFormContext(); + const { requiredFields = [] } = useFormContextInternal() ?? {}; if (!label && !!tooltip) { throw new Error("You can't have a tooltip without a label"); @@ -23,7 +24,7 @@ const FormGroupLayoutLabel = (props: FormGroupLayoutLabel return null; } - const fieldIsRequired = typeof label == "string" && requiredFields.includes(fieldName); + const fieldIsRequired = typeof label == "string" && requiredFields.includes(fieldName as Path); const finalLabel = fieldIsRequired ? `${String(label)} *` : label; const switchLayout = layout === "switch"; diff --git a/src/lib/FormattedInput.tsx b/src/lib/FormattedInput.tsx index a6f5bee..4b9ec11 100644 --- a/src/lib/FormattedInput.tsx +++ b/src/lib/FormattedInput.tsx @@ -1,24 +1,20 @@ import classnames from "classnames"; -import { Controller, FieldValues } from "react-hook-form"; +import { Controller, ControllerRenderProps, FieldValues } from "react-hook-form"; import { NumericFormat, NumericFormatProps, PatternFormat, PatternFormatProps } from "react-number-format"; import { useSafeNameId } from "src/lib/hooks/useSafeNameId"; import { FormGroupLayout } from "./FormGroupLayout"; import { CommonInputProps } from "./types/CommonInputProps"; import { useMarkOnFocusHandler } from "./hooks/useMarkOnFocusHandler"; -import { useFormContext } from "./context/FormContext"; - -interface FormattedInputProps extends CommonInputProps { - patternFormat?: PatternFormatProps; - numericFormat?: NumericFormatProps; -} - -const FormattedInput = (props: FormattedInputProps) => { - if (props.patternFormat && props.numericFormat) { - throw new Error("FormattedInput cannot have both patternFormat and numericFormat"); - } +import { UnknownType, useFormContextInternal } from "./context/FormContext"; +type FormattedInputInternalProps = Omit, "name" | "disabled"> & { + name: string; + isDisabled?: boolean; + commonProps?: NumericFormatProps; + fieldOnChange?: ControllerRenderProps["onChange"]; +}; +const FormattedInputInternal = (props: FormattedInputInternalProps) => { const { - disabled, label, helpText, numericFormat, @@ -33,14 +29,94 @@ const FormattedInput = (props: FormattedInputProps) => addonRight, className = "", hideValidationMessage = false, + defaultValue, + name, + id, + isDisabled, + commonProps, + fieldOnChange, } = props; - const { name, id } = useSafeNameId(props.name, props.id); - const { control, disabled: formDisabled } = useFormContext(); + const focusHandler = useMarkOnFocusHandler(markAllOnFocus); + const commonPropsInternal = commonProps ?? { + onBlur: (e) => { + if (propsOnBlur) { + propsOnBlur(e); + } + }, + disabled: isDisabled, + className: classnames("form-control", className), + }; + + return ( + + <> + {numericFormat && ( + { + if (propsOnChange) propsOnChange(e); + }} + onValueChange={(values) => { + fieldOnChange && fieldOnChange(values.value); + }} + onFocus={focusHandler} + style={style} + > + )} + + {patternFormat && ( + + )} + + + ); +}; + +type FormattedInputProps = CommonInputProps & { + patternFormat?: PatternFormatProps; + numericFormat?: NumericFormatProps; +}; + +const FormattedInput = (props: FormattedInputProps) => { + if (props.patternFormat && props.numericFormat) { + throw new Error("FormattedInput cannot have both patternFormat and numericFormat"); + } + + const { + disabled, + onBlur: propsOnBlur, + className = "", + } = props; + + const { name, id } = useSafeNameId(props?.name ?? "", props.id); + const { control, disabled: formDisabled = false } = useFormContextInternal() ?? {}; const isDisabled = formDisabled || disabled; - return ( + return control ? ( (props: FormattedInputProps) => if (propsOnBlur) propsOnBlur(e); onBlur(); }, + onValueChange: (values) => { + onChange(values.value); + }, disabled: isDisabled, }; - return ( - - <> - {numericFormat && ( - { - if (propsOnChange) propsOnChange(e); - }} - onValueChange={(values) => { - onChange(values.value); - }} - onFocus={focusHandler} - style={style} - > - )} - - {patternFormat && ( - - )} - - - ); + return ; }} /> + ) : ( + ); }; diff --git a/src/lib/Input.tsx b/src/lib/Input.tsx index a53dec7..16d70fb 100644 --- a/src/lib/Input.tsx +++ b/src/lib/Input.tsx @@ -7,12 +7,12 @@ import { FormGroupLayout } from "./FormGroupLayout"; import { InputInternal } from "./InputInternal"; import { CommonInputProps } from "./types/CommonInputProps"; import { LabelValueOption } from "./types/LabelValueOption"; -import { useFormContext } from "./context/FormContext"; +import { UnknownType, useFormContextInternal } from "./context/FormContext"; import { MutableRefObject } from "react"; const invalidAddonTypes = ["switch", "radio", "checkbox"]; -interface InputProps extends CommonInputProps { +type InputProps = CommonInputProps & { type?: InputType; options?: LabelValueOption[]; multiple?: boolean; @@ -26,7 +26,7 @@ interface InputProps extends CommonInputProps { innerRef?: MutableRefObject; } -const Input = (props: InputProps) => { +const Input = (props: InputProps) => { const { type, options, addonLeft, name, addonRight, rangeMin, rangeMax, textAreaRows, multiple, id, value, disabled, step } = props; if (type === "radio" && !options) { @@ -67,9 +67,8 @@ const Input = (props: InputProps) => { return undefined; })(); - const { id: safeId } = useSafeNameId(name, id); - const { disabled: formDisabled } = useFormContext(); - + const { id: safeId } = useSafeNameId(name ?? "", id); + const { disabled: formDisabled = false } = useFormContextInternal() ?? {}; const isDisabled = formDisabled || disabled; return ( @@ -102,7 +101,7 @@ const Input = (props: InputProps) => { ) : ( <> - + )} diff --git a/src/lib/InputInternal.tsx b/src/lib/InputInternal.tsx index 254a26a..2cac783 100644 --- a/src/lib/InputInternal.tsx +++ b/src/lib/InputInternal.tsx @@ -1,14 +1,14 @@ -import { FieldValues, get, FieldError } from "react-hook-form"; +import { FieldValues, get, FieldError, UseFormRegisterReturn } from "react-hook-form"; import { Input } from "reactstrap"; import { useSafeNameId } from "src/lib/hooks/useSafeNameId"; import { InputProps } from "./Input"; import { useMarkOnFocusHandler } from "./hooks/useMarkOnFocusHandler"; -import { useFormContext } from "./context/FormContext"; +import { UnknownType, useFormContextInternal } from "./context/FormContext"; // This is two random guids concatenated. It is used to set the value of the option to undefined. const UNDEFINED_OPTION_VALUE = "CABB7A27DB754DA58C89D43ADB03FE0EC5EE3E25A6624D749F35CF2E92CFA784"; -const InputInternal = (props: InputProps) => { +const InputInternal = (props: InputProps) => { const { disabled, type, @@ -27,20 +27,21 @@ const InputInternal = (props: InputProps) => { className, style, innerRef, + defaultValue } = props; - const { name, id } = useSafeNameId(props.name, props.id); + const { name, id } = useSafeNameId(props.name ?? "", props.id); const focusHandler = useMarkOnFocusHandler(markAllOnFocus); const { register, - formState: { errors }, - disabled: formDisabled, - } = useFormContext(); + formState, + disabled: formDisabled = false, + } = useFormContextInternal() ?? {}; - const { ref, ...rest } = register(name, { + const { ref, ...rest } =register ? register(name, { setValueAs: (value: string) => (value === UNDEFINED_OPTION_VALUE ? undefined : value), - }); + }) : {} as Partial; // probably to write better - const fieldError = get(errors, name) as FieldError | undefined; + const fieldError = formState ? get(formState.errors, name) as FieldError | undefined : undefined; const hasError = !!fieldError; return ( @@ -53,7 +54,7 @@ const InputInternal = (props: InputProps) => { if (innerRef) { innerRef.current = elem; } - ref(elem); + ref && ref(elem); }} min={rangeMin} max={rangeMax} @@ -64,6 +65,7 @@ const InputInternal = (props: InputProps) => { style={plainText ? { color: "black", marginLeft: 10, ...style } : { ...style }} placeholder={placeholder} step={step} + defaultValue={defaultValue} {...rest} {...(value ? { value } : {})} onBlur={(e) => { @@ -72,7 +74,9 @@ const InputInternal = (props: InputProps) => { onBlur(e); } - await rest.onBlur(e); + if (rest?.onBlur) { + await rest.onBlur(e); + } })(); }} onChange={(e) => { @@ -81,7 +85,9 @@ const InputInternal = (props: InputProps) => { onChange(e); } - await rest.onChange(e); + if (rest?.onChange) { + await rest.onChange(e); + } })(); }} onFocus={focusHandler} diff --git a/src/lib/StaticTypeaheadInput.tsx b/src/lib/StaticTypeaheadInput.tsx index eaff897..63e5b56 100644 --- a/src/lib/StaticTypeaheadInput.tsx +++ b/src/lib/StaticTypeaheadInput.tsx @@ -11,7 +11,7 @@ import { useFormContext } from "./context/FormContext"; import { useRef } from "react"; import { LabelValueOption } from "./types/LabelValueOption"; -interface StaticTypeaheadInputProps extends CommonTypeaheadProps { +type StaticTypeaheadInputProps = CommonTypeaheadProps & { options: TypeaheadOptions; reactBootstrapTypeaheadProps?: Partial; } @@ -39,7 +39,7 @@ const StaticTypeaheadInput = (props: StaticTypeaheadInput inputRef, useGroupBy = false, } = props; - const { name, id } = useSafeNameId(props.name, props.id); + const { name, id } = useSafeNameId(props?.name ?? "", props.id); const { control, disabled: formDisabled, diff --git a/src/lib/context/FormContext.tsx b/src/lib/context/FormContext.tsx index 626673f..06724f8 100644 --- a/src/lib/context/FormContext.tsx +++ b/src/lib/context/FormContext.tsx @@ -7,6 +7,8 @@ export interface FormContextProps extends UseFormReturn; + // eslint-disable-next-line @typescript-eslint/no-explicit-any export const FormContext = createContext | null>(null); @@ -17,3 +19,8 @@ export const useFormContext = () => { } return context as FormContextProps; }; + +export const useFormContextInternal = () => { + const context = useContext(FormContext); + return context as Partial> | undefined; +}; \ No newline at end of file diff --git a/src/lib/types/CommonInputProps.ts b/src/lib/types/CommonInputProps.ts index fd9fbad..1fc1e57 100644 --- a/src/lib/types/CommonInputProps.ts +++ b/src/lib/types/CommonInputProps.ts @@ -1,5 +1,6 @@ import { ReactNode, CSSProperties } from "react"; import { FieldPath, FieldValues } from "react-hook-form"; +import { UnknownType } from "../context/FormContext"; interface DefaultAddonProps { isDisabled?: boolean; @@ -7,17 +8,18 @@ interface DefaultAddonProps { export type MergedAddonProps = TRenderAddon & DefaultAddonProps; -interface CommonInputProps { +interface CommonInputPropsInternal { onChange?: (e: React.ChangeEvent) => void; onBlur?: (e: React.FocusEvent) => void; label?: ReactNode; - name: FieldPath; + name: T extends UnknownType ? string : FieldPath; id?: string; helpText?: ReactNode; disabled?: boolean; labelToolTip?: string; markAllOnFocus?: boolean; inputOnly?: boolean; + defaultValue?: T extends UnknownType ? string | number : never; /** * Component prop that represents an additional className attribute @@ -76,4 +78,7 @@ interface CommonInputProps { hideValidationMessage?: boolean; } +type CommonInputProps = T extends UnknownType + ? Omit, "name"> & { name?: string } + : CommonInputPropsInternal; export { CommonInputProps }; diff --git a/src/lib/types/Typeahead.ts b/src/lib/types/Typeahead.ts index 66fd8b6..d72e4a8 100644 --- a/src/lib/types/Typeahead.ts +++ b/src/lib/types/Typeahead.ts @@ -6,7 +6,7 @@ import TypeheadRef from "react-bootstrap-typeahead/types/core/Typeahead"; export type TypeaheadOptions = LabelValueOption[] | string[]; -interface CommonTypeaheadProps extends Omit, "onChange"> { +type CommonTypeaheadProps = Omit, "onChange"> & { multiple?: boolean; emptyLabel?: string; invalidErrorMessage?: string;