Skip to content

Commit

Permalink
fix: Portal frontend 31 (#53)
Browse files Browse the repository at this point in the history
* make subheader optional

* provide values

* submit form helper

* dirty utils

* auth utils

* add path helpers
  • Loading branch information
SKairinos authored Aug 13, 2024
1 parent a0857b6 commit 9cb70db
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 27 deletions.
20 changes: 13 additions & 7 deletions src/components/page/Banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Section from "./Section"

export interface BannerProps {
header: string
subheader: string
subheader?: string
textAlign?: "start" | "center"
imageProps?: ImageProps
buttonProps?: ButtonProps
Expand Down Expand Up @@ -50,16 +50,22 @@ const Banner: FC<BannerProps> = ({
}}
textAlign={textAlign}
>
<Typography variant="h2" color={contrastText}>
{header}
</Typography>
<Typography
variant="h2"
color={contrastText}
variant="h4"
mb={buttonProps !== undefined ? undefined : 0}
mb={subheader !== undefined ? undefined : 0}
>
{subheader}
{header}
</Typography>
{subheader !== undefined && (
<Typography
color={contrastText}
variant="h4"
mb={buttonProps !== undefined ? undefined : 0}
>
{subheader}
</Typography>
)}
{buttonProps !== undefined && <Button {...buttonProps} />}
</Stack>
{imageProps !== undefined && (
Expand Down
11 changes: 11 additions & 0 deletions src/utils/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Cookies from "js-cookie"

export function logout() {
Cookies.remove("session_key")
Cookies.remove("session_metadata")
}

// https://docs.djangoproject.com/en/3.2/ref/csrf/
export function getCsrfCookie() {
return Cookies.get(`${import.meta.env.VITE_SERVICE_NAME}_csrftoken`)
}
96 changes: 77 additions & 19 deletions src/utils/form.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import type {
TypedLazyQueryTrigger,
TypedMutationTrigger,
} from "@reduxjs/toolkit/query/react"
import type { TypedMutationTrigger } from "@reduxjs/toolkit/query/react"
import type { FieldValidator, FormikHelpers } from "formik"
import { ValidationError, type Schema, type ValidateOptions } from "yup"

import { excludeKeyPaths } from "./general"

export function isFormError(error: unknown): boolean {
return (
typeof error === "object" &&
Expand Down Expand Up @@ -33,29 +32,66 @@ export function setFormErrors(
setErrors(data)
}

export function submitForm<QueryArg, ResultType, FormValues extends QueryArg>(
trigger:
| TypedMutationTrigger<ResultType, QueryArg, any>
| TypedLazyQueryTrigger<ResultType, QueryArg, any>,
query?: {
then?: (result: ResultType) => void
catch?: (error: Error) => void
finally?: () => void
},
): (
export type SubmitFormOptions<
QueryArg extends object,
ResultType,
FormValues extends QueryArg,
> = Partial<{
clean: (values: FormValues) => QueryArg
exclude: string[]
then: (
result: ResultType,
values: FormValues,
helpers: FormikHelpers<FormValues>,
) => void
catch: (
error: unknown,
values: FormValues,
helpers: FormikHelpers<FormValues>,
) => void
finally: (values: FormValues, helpers: FormikHelpers<FormValues>) => void
}>

export type SubmitFormHandler<
QueryArg extends object,
FormValues extends QueryArg,
> = (
values: FormValues,
helpers: FormikHelpers<FormValues>,
) => void | Promise<any> {
) => void | Promise<any>

export function submitForm<
QueryArg extends object,
ResultType,
FormValues extends QueryArg,
>(
trigger: TypedMutationTrigger<ResultType, QueryArg, any>,
options?: SubmitFormOptions<QueryArg, ResultType, FormValues>,
): SubmitFormHandler<QueryArg, FormValues> {
const {
clean,
exclude,
then,
catch: _catch,
finally: _finally,
} = options || {}

return (values, helpers) => {
trigger(values)
let arg: QueryArg = clean ? clean(values) : values

if (exclude) arg = excludeKeyPaths(arg, exclude)

trigger(arg)
.unwrap()
.then(query?.then)
.then(result => {
if (then) then(result, values, helpers)
})
.catch(error => {
if (query?.catch !== undefined) query.catch(error)
if (_catch) _catch(error, values, helpers)
setFormErrors(error, helpers.setErrors)
})
.finally(() => {
if (query?.finally !== undefined) query.finally()
if (_finally) _finally(values, helpers)
})
}
}
Expand All @@ -76,3 +112,25 @@ export function schemaToFieldValidator(
}
}
}

// Checking if individual fields are dirty is not currently supported.
// https://github.com/jaredpalmer/formik/issues/1421
export function getDirty<
Values extends Record<string, any>,
Names extends Array<keyof Values>,
>(
values: Values,
initialValues: Values,
names: Names,
): Record<Names[number], boolean> {
return Object.fromEntries(
names.map(name => [name, isDirty(values, initialValues, name)]),
) as Record<Names[number], boolean>
}

export function isDirty<
Values extends Record<string, any>,
Name extends keyof Values,
>(values: Values, initialValues: Values, name: Name): boolean {
return values[name] !== initialValues[name]
}
23 changes: 22 additions & 1 deletion src/utils/general.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { getNestedProperty } from "./general"
import { excludeKeyPaths, getNestedProperty, withKeyPaths } from "./general"

// getNestedProperty

const PERSON = { father: { father: { name: "John" } } }

Expand All @@ -19,3 +21,22 @@ test("get a nested property that doesn't exist", () => {

expect(name).toBeUndefined()
})

// withKeyPaths

test("get the paths of nested keys", () => {
const obj = withKeyPaths({ a: 1, b: { c: 2, d: { e: 3 } } })

expect(obj).toMatchObject({ a: 1, b: { "b.c": 2, "b.d": { "b.d.e": 3 } } })
})

// excludeKeyPaths

test("exclude nested keys by their path", () => {
const obj = excludeKeyPaths({ a: 1, b: { c: 2, d: { e: 3 } } }, [
"b.c",
"b.d.e",
])

expect(obj).toMatchObject({ a: 1, b: { d: {} } })
})
38 changes: 38 additions & 0 deletions src/utils/general.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,3 +453,41 @@ export function getNestedProperty(

return value
}

export function withKeyPaths(obj: object, delimiter: string = "."): object {
function _withKeyPaths(obj: object, path: string[]) {
return Object.fromEntries(
Object.entries(obj).map(([key, value]) => {
const _path = [...path, key]

if (typeof value === "object") value = _withKeyPaths(value, _path)

return [_path.join(delimiter), value]
}),
)
}

return _withKeyPaths(obj, [])
}

export function excludeKeyPaths(
obj: object,
exclude: string[],
delimiter: string = ".",
): any {
function _excludeKeyPaths(obj: object, path: string[]) {
return Object.fromEntries(
Object.entries(obj)
.map(([key, value]) => {
const _path = [...path, key]

if (typeof value === "object") value = _excludeKeyPaths(value, _path)

return exclude.includes(_path.join(delimiter)) ? [] : [key, value]
})
.filter(entry => entry.length),
)
}

return exclude.length ? _excludeKeyPaths(obj, []) : obj
}

0 comments on commit 9cb70db

Please sign in to comment.