diff --git a/package.json b/package.json index 4eed6b0ae..fc98b7353 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,8 @@ "dependencies": { "@datadog/browser-logs": "^4.21.2", "@heroicons/react": "^1.0.6", + "@stripe/react-stripe-js": "1.13.0", + "@stripe/stripe-js": "1.41.0", "apexcharts": "^3.36.0", "axios": "^1.1.2", "browser-cookies": "^1.2.0", @@ -78,8 +80,6 @@ "@babel/preset-typescript": "^7.18.6", "@babel/runtime": "^7.19.4", "@cypress/code-coverage": "^3.10.0", - "@stripe/react-stripe-js": "1.13.0", - "@stripe/stripe-js": "1.41.0", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^14.4.3", @@ -144,6 +144,7 @@ "start-server-and-test": "^1.14.0", "style-loader": "^3.3.1", "systemjs-webpack-interop": "^2.3.7", + "typed-scss-modules": "^7.0.1", "webpack": "^4.41.2", "webpack-cli": "^4.10.0", "webpack-config-single-spa-react": "^4.0.3", diff --git a/src-ts/declarations.d.ts b/src-ts/declarations.d.ts index 84ecabc9c..71bd1d6e3 100644 --- a/src-ts/declarations.d.ts +++ b/src-ts/declarations.d.ts @@ -7,11 +7,6 @@ declare module '*.html' { declare module '*.pdf' -declare module '*.scss' { - const scssFile: { [style: string]: any } - export = scssFile -} - declare module '*.svg' { import * as React from 'react' diff --git a/src-ts/header/utility-selectors/UtilitySelector/ProfileSelector/profile-not-logged-in/ProfileNotLoggedIn.tsx b/src-ts/header/utility-selectors/UtilitySelector/ProfileSelector/profile-not-logged-in/ProfileNotLoggedIn.tsx index 435f0e0f1..d0b577aee 100644 --- a/src-ts/header/utility-selectors/UtilitySelector/ProfileSelector/profile-not-logged-in/ProfileNotLoggedIn.tsx +++ b/src-ts/header/utility-selectors/UtilitySelector/ProfileSelector/profile-not-logged-in/ProfileNotLoggedIn.tsx @@ -1,4 +1,4 @@ -import { FC, useContext } from 'react' +import { FC, useCallback, useContext } from 'react' import { Location, useLocation } from 'react-router-dom' import { @@ -14,10 +14,13 @@ const ProfileNotLoggedIn: FC<{}> = () => { const routeData: RouteContextData = useContext(routeContext) const location: Location = useLocation() - function signUp(): void { + const signUpHandler: () => void = useCallback(() => { const signupUrl: string = routeData.getSignupUrl(location.pathname, routeData.toolsRoutes) window.location.href = signupUrl - } + }, [ + location.pathname, + routeData, + ]) return ( <> @@ -34,7 +37,7 @@ const ProfileNotLoggedIn: FC<{}> = () => { label='Sign Up' size='md' tabIndex={-1} - onClick={signUp} + onClick={signUpHandler} /> ) diff --git a/src-ts/lib/breadcrumb/breadcrumb-item/BreadcrumbItem.tsx b/src-ts/lib/breadcrumb/breadcrumb-item/BreadcrumbItem.tsx index 43e43a236..076b9ecae 100644 --- a/src-ts/lib/breadcrumb/breadcrumb-item/BreadcrumbItem.tsx +++ b/src-ts/lib/breadcrumb/breadcrumb-item/BreadcrumbItem.tsx @@ -10,12 +10,25 @@ interface BreadcrumbItemProps { item: BreadcrumbItemModel } -const BreadcrumbItem: FC = (props: BreadcrumbItemProps) => ( -
  • props.item.onClick?.(props.item)}> - - {props.item.name} - -
  • -) +const BreadcrumbItem: FC = (props: BreadcrumbItemProps) => { + + function onClick(): void { + props.item.onClick?.(props.item) + } + + return ( +
  • onClick()} + > + + {props.item.name} + +
  • + ) +} export default BreadcrumbItem diff --git a/src-ts/lib/button/Button.tsx b/src-ts/lib/button/Button.tsx index e465f0696..fffac4015 100644 --- a/src-ts/lib/button/Button.tsx +++ b/src-ts/lib/button/Button.tsx @@ -43,7 +43,7 @@ const Button: FC = (props: ButtonProps) => { className={classes} href={props.url} onClick={clickHandler} - rel={props.rel ?? props.target === '_blank' ? 'noreferrer' : ''} + rel={props.rel || props.target === '_blank' ? 'noreferrer' : ''} role='button' tabIndex={props.tabIndex} title={props.title} @@ -104,14 +104,20 @@ const Button: FC = (props: ButtonProps) => { ) } -function getButtonClasses(props: ButtonProps): string { +function getButtonClasses( + className: string, + buttonStyle: ButtonStyle, + size: ButtonSize, + disable?: boolean, + hidden?: boolean, +): string { const classes: string = classNames( 'button', - props.className, - props.buttonStyle || 'primary', - `button-${props.size || 'md'}`, - !!props.disable ? 'disabled' : undefined, - props.hidden ? 'hidden' : undefined, + className, + buttonStyle || 'primary', + `button-${size || 'md'}`, + { disabled: disable }, + { hidden }, ) return classes } diff --git a/src-ts/lib/form/Form.tsx b/src-ts/lib/form/Form.tsx index 857c9b766..07de469e4 100644 --- a/src-ts/lib/form/Form.tsx +++ b/src-ts/lib/form/Form.tsx @@ -23,8 +23,9 @@ import { formOnChange, formOnReset, formOnSubmitAsync, + formValidateForm, + FormValue, } from './form-functions' -import { validateForm } from './form-functions/form.functions' import { FormGroups } from './form-groups' import styles from './Form.module.scss' @@ -41,8 +42,8 @@ interface FormProps { readonly shouldDisableButton?: (isPrimaryGroup: boolean, index: number) => boolean } -const Form: (props: FormProps) => JSX.Element - = (props: FormProps) => { +const Form: (props: FormProps) => JSX.Element + = (props: FormProps) => { const [formDef, setFormDef]: [FormDefinition, Dispatch>] = useState({ ...props.formDef }) @@ -65,7 +66,7 @@ const Form: (props: FormProps(props: FormProps(props: FormProps): void { @@ -167,7 +169,7 @@ const Form: (props: FormProps ) }) diff --git a/src-ts/lib/form/form-functions/form-value.model.ts b/src-ts/lib/form/form-functions/form-value.model.ts new file mode 100644 index 000000000..7b8e1e5f8 --- /dev/null +++ b/src-ts/lib/form/form-functions/form-value.model.ts @@ -0,0 +1,5 @@ +import { InputValue } from '../form-input.model' + +export interface FormValue { + [propertyName: string]: InputValue, +} diff --git a/src-ts/lib/form/form-functions/form.functions.ts b/src-ts/lib/form/form-functions/form.functions.ts index cec5c58c5..f8e089601 100644 --- a/src-ts/lib/form/form-functions/form.functions.ts +++ b/src-ts/lib/form/form-functions/form.functions.ts @@ -3,9 +3,13 @@ import { toast } from 'react-toastify' import { FormAction, FormDefinition } from '../form-definition.model' import { FormGroup } from '../form-group.model' -import { FormInputModel } from '../form-input.model' +import { FormInputModel, InputValue } from '../form-input.model' -export function getInputElement(formElements: HTMLFormControlsCollection, fieldName: string): HTMLInputElement { +import { FormValue } from './form-value.model' + +export type ValidationEvent = 'blur' | 'change' | 'submit' | 'initial' + +export function getInputElement(formElements: HTMLFormControlsCollection, fieldName: string): HTMLInputElement | undefined { return formElements.namedItem(fieldName) as HTMLInputElement } @@ -29,29 +33,43 @@ export function getInputModel(inputs: ReadonlyArray, fieldName: return formField } -export function initializeValues(inputs: Array, formValues?: T): void { - inputs +export function initializeValues( + inputs: Array, + formValues?: T, +): void { + + const filteredInputs: ReadonlyArray = inputs .filter(input => !input.dirty && !input.touched) - .forEach(input => { - if (input.type === 'checkbox') { - input.value = input.checked || false - } else { - input.value = !!(formValues as any)?.hasOwnProperty(input.name) - ? (formValues as any)[input.name] + + for (const input of filteredInputs) { + if (input.type === 'checkbox') { + input.value = input.checked || false + } else { + input.value + = !!formValues && Object.prototype.hasOwnProperty.call(formValues, input.name) + ? (formValues as { [id: string]: InputValue })[input.name] : undefined - } - }) + } + } } -export function onBlur(event: FormEvent, inputs: ReadonlyArray, formValues?: T): void { +export function onBlur( + event: FormEvent, + inputs: ReadonlyArray, + formValues?: T, +): void { handleFieldEvent(event.target as HTMLInputElement | HTMLTextAreaElement, inputs, 'blur', formValues) } -export function onChange(event: ChangeEvent, inputs: ReadonlyArray, formValues?: T): void { +export function onChange( + event: ChangeEvent, + inputs: ReadonlyArray, + formValues?: T, +): void { handleFieldEvent(event.target as HTMLInputElement | HTMLTextAreaElement, inputs, 'change', formValues) } -export function onReset(inputs: ReadonlyArray, formValue?: any): void { +export function onReset(inputs: ReadonlyArray, formValue?: T): void { inputs?.forEach(inputDef => { const typeCastedInput: FormInputModel = inputDef as FormInputModel typeCastedInput.dirty = false @@ -61,7 +79,7 @@ export function onReset(inputs: ReadonlyArray, formValue?: any): }) } -export async function onSubmitAsync( +export async function onSubmitAsync( action: FormAction, event: FormEvent, formDef: FormDefinition, @@ -93,13 +111,14 @@ export async function onSubmitAsync( } // set the properties for the updated T value + const updatedValue: FormValue = { ...formValue } inputs .forEach(field => { - (formValue as any)[field.name] = field.value + updatedValue[field.name] = field.value }) // if there are no dirty fields, don't actually perform the save - const savePromise: Promise = !dirty ? Promise.resolve() : save(formValue) + const savePromise: Promise = !dirty ? Promise.resolve() : save(updatedValue as T) return savePromise .then(() => { @@ -109,13 +128,18 @@ export async function onSubmitAsync( toast.success(safeSuccessMessage) onSuccess?.() }) - .catch(error => Promise.reject(error.response?.data?.result?.content || error.message || error)) + .catch(error => Promise.reject(error.response?.data?.result?.content ?? error.message ?? error)) } -function handleFieldEvent(input: HTMLInputElement | HTMLTextAreaElement, inputs: ReadonlyArray, event: 'blur' | 'change', formValues?: T): void { +function handleFieldEvent( + input: HTMLInputElement | HTMLTextAreaElement, + inputs: ReadonlyArray, + event: 'blur' | 'change', + formValues?: T, +): void { // set the dirty and touched flags on the field - const originalValue: string | undefined = (formValues as any)?.[input.name] + const originalValue: InputValue = formValues?.[input.name] const inputDef: FormInputModel = getInputModel(inputs, input.name) @@ -153,7 +177,11 @@ function handleFieldEvent(input: HTMLInputElement | HTMLTextAreaElement, inpu }) } -function validateField(formInputDef: FormInputModel, formElements: HTMLFormControlsCollection, event: 'blur' | 'change' | 'submit' | 'initial'): void { +function validateField( + formInputDef: FormInputModel, + formElements: HTMLFormControlsCollection, + event: 'blur' | 'change' | 'submit' | 'initial', +): void { // this is the error the field had before the event took place const previousError: string | undefined = formInputDef.error @@ -162,7 +190,11 @@ function validateField(formInputDef: FormInputModel, formElements: HTMLFormContr ?.forEach(validatorFunction => { // if the next error is the same as the previous error, then no need to do anything - const nextError: string | undefined = validatorFunction.validator(formInputDef.value, formElements, validatorFunction.dependentField) + const nextError: string | undefined = validatorFunction.validator( + formInputDef.value, + formElements, + validatorFunction.dependentField, + ) if (previousError === nextError) { return @@ -186,13 +218,19 @@ function validateField(formInputDef: FormInputModel, formElements: HTMLFormContr }) } -export type ValidationEvent = 'blur' | 'change' | 'submit' | 'initial' +export function validateForm( + formElements: HTMLFormControlsCollection, + event: ValidationEvent, + inputs: ReadonlyArray, +): boolean { + + let hasError: boolean = false -export function validateForm(formElements: HTMLFormControlsCollection, event: ValidationEvent, inputs: ReadonlyArray): boolean { - const errors: ReadonlyArray = inputs?.filter(formInputDef => { + for (const formInputDef of inputs) { formInputDef.dirty = formInputDef.dirty || event === 'submit' validateField(formInputDef, formElements, event) - return !!formInputDef.error - }) - return !errors.length + hasError = hasError || !!formInputDef.error + } + + return !hasError } diff --git a/src-ts/lib/form/form-functions/index.ts b/src-ts/lib/form/form-functions/index.ts index 5928e9090..e763c2827 100644 --- a/src-ts/lib/form/form-functions/index.ts +++ b/src-ts/lib/form/form-functions/index.ts @@ -7,4 +7,6 @@ export { onReset as formOnReset, onSubmitAsync as formOnSubmitAsync, getFormInputFields as formGetInputFields, + validateForm as formValidateForm, } from './form.functions' +export * from './form-value.model' diff --git a/src-ts/lib/form/form-groups/form-card-set/FormCardSet.tsx b/src-ts/lib/form/form-groups/form-card-set/FormCardSet.tsx index 9da8a4824..59c247986 100644 --- a/src-ts/lib/form/form-groups/form-card-set/FormCardSet.tsx +++ b/src-ts/lib/form/form-groups/form-card-set/FormCardSet.tsx @@ -1,7 +1,15 @@ import React, { FocusEvent, SVGProps } from 'react' import cn from 'classnames' -import { Button, IconCheck, IconOutline, IconSolid, textFormatMoneyLocaleString, Tooltip, useCheckIsMobile } from '../../..' +import { + Button, + IconCheck, + IconOutline, + IconSolid, + textFormatMoneyLocaleString, + Tooltip, + useCheckIsMobile, +} from '../../..' import { FormCard, FormInputModel } from '../../form-input.model' import styles from './FormCardSet.module.scss' @@ -10,7 +18,8 @@ interface FormCardSetProps extends FormInputModel { readonly onChange: (event: FocusEvent) => void } -const FormCardSet: React.FC = ({ name, cards, onChange, value }: FormCardSetProps) => { +const FormCardSet: React.FC = (props: FormCardSetProps) => { + const isMobile: boolean = useCheckIsMobile() const iconFromName: (icon: string) => JSX.Element = (icon: string) => { @@ -25,16 +34,14 @@ const FormCardSet: React.FC = ({ name, cards, onChange, value const getButton: (card: FormCard, selected: boolean) => JSX.Element = (card, selected) => (