codeforlife 2.6.5 → 2.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/src/api/createApi.ts +9 -2
- package/src/components/form/Form.tsx +92 -15
- package/src/hooks/form.tsx +11 -0
- package/src/hooks/index.ts +1 -0
- package/src/utils/api.tsx +5 -0
- package/src/utils/form.test.ts +50 -0
- package/src/utils/form.ts +48 -13
- package/src/utils/general.test.ts +15 -2
- package/src/utils/general.ts +19 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [2.6.7](https://github.com/ocadotechnology/codeforlife-package-javascript/compare/v2.6.6...v2.6.7) (2025-02-19)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* Package javascript 76 ([#77](https://github.com/ocadotechnology/codeforlife-package-javascript/issues/77)) ([73c7113](https://github.com/ocadotechnology/codeforlife-package-javascript/commit/73c7113d50f7e1bff64655cf30da30013f051950))
|
|
7
|
+
|
|
8
|
+
## [2.6.6](https://github.com/ocadotechnology/codeforlife-package-javascript/compare/v2.6.5...v2.6.6) (2025-01-27)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* csrf header for non-safe http methods ([#75](https://github.com/ocadotechnology/codeforlife-package-javascript/issues/75)) ([d0b2b78](https://github.com/ocadotechnology/codeforlife-package-javascript/commit/d0b2b7852fbdc9f84ade5ec4d46cc8a980e60f1e))
|
|
14
|
+
|
|
1
15
|
## [2.6.5](https://github.com/ocadotechnology/codeforlife-package-javascript/compare/v2.6.4...v2.6.5) (2025-01-17)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
package/src/api/createApi.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createApi as _createApi,
|
|
3
3
|
fetchBaseQuery,
|
|
4
|
+
type FetchArgs,
|
|
4
5
|
} from "@reduxjs/toolkit/query/react"
|
|
5
6
|
|
|
6
7
|
import { SERVICE_API_URL } from "../settings"
|
|
7
8
|
import defaultTagTypes from "./tagTypes"
|
|
8
9
|
import { buildLogoutEndpoint } from "./endpoints/session"
|
|
9
10
|
import { getCsrfCookie } from "../utils/auth"
|
|
11
|
+
import { isSafeHttpMethod } from "../utils/api"
|
|
10
12
|
|
|
11
13
|
// TODO: decide if we want to keep any of this.
|
|
12
14
|
// export function handleResponseError(error: FetchBaseQueryError): void {
|
|
@@ -36,8 +38,13 @@ export default function createApi<TagTypes extends string = never>({
|
|
|
36
38
|
const fetch = fetchBaseQuery({
|
|
37
39
|
baseUrl: `${SERVICE_API_URL}/`,
|
|
38
40
|
credentials: "include",
|
|
39
|
-
prepareHeaders: (headers,
|
|
40
|
-
|
|
41
|
+
prepareHeaders: (headers, endpoint) => {
|
|
42
|
+
const { type, arg } = endpoint as typeof endpoint & {
|
|
43
|
+
arg: string | FetchArgs
|
|
44
|
+
}
|
|
45
|
+
const method = typeof arg === "string" ? "GET" : arg.method || "GET"
|
|
46
|
+
|
|
47
|
+
if (type === "mutation" || !isSafeHttpMethod(method)) {
|
|
41
48
|
let csrfToken = getCsrfCookie()
|
|
42
49
|
if (csrfToken) headers.set("x-csrftoken", csrfToken)
|
|
43
50
|
}
|
|
@@ -1,27 +1,104 @@
|
|
|
1
|
+
import { FormHelperText, type FormHelperTextProps } from "@mui/material"
|
|
1
2
|
import {
|
|
2
3
|
Formik,
|
|
3
4
|
Form as FormikForm,
|
|
4
5
|
type FormikConfig,
|
|
5
6
|
type FormikErrors,
|
|
7
|
+
type FormikProps,
|
|
6
8
|
} from "formik"
|
|
9
|
+
import {
|
|
10
|
+
type ReactNode,
|
|
11
|
+
type FC,
|
|
12
|
+
useRef,
|
|
13
|
+
useEffect,
|
|
14
|
+
type RefObject,
|
|
15
|
+
} from "react"
|
|
7
16
|
import type { TypedUseMutation } from "@reduxjs/toolkit/query/react"
|
|
8
17
|
|
|
18
|
+
import { getKeyPaths } from "../../utils/general"
|
|
9
19
|
import {
|
|
10
20
|
submitForm,
|
|
11
21
|
type SubmitFormOptions,
|
|
12
22
|
type FormValues,
|
|
13
23
|
} from "../../utils/form"
|
|
14
24
|
|
|
15
|
-
const
|
|
25
|
+
const SCROLL_INTO_VIEW_OPTIONS: ScrollIntoViewOptions = {
|
|
26
|
+
behavior: "smooth",
|
|
27
|
+
block: "start",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type NonFieldErrorsProps = Omit<FormHelperTextProps, "error" | "ref"> & {
|
|
31
|
+
scrollIntoViewOptions?: ScrollIntoViewOptions
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const NonFieldErrors: FC<NonFieldErrorsProps> = ({
|
|
35
|
+
scrollIntoViewOptions = SCROLL_INTO_VIEW_OPTIONS,
|
|
36
|
+
...formHelperTextProps
|
|
37
|
+
}) => {
|
|
38
|
+
const pRef = useRef<HTMLParagraphElement>(null)
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (pRef.current) pRef.current.scrollIntoView(scrollIntoViewOptions)
|
|
42
|
+
}, [scrollIntoViewOptions])
|
|
43
|
+
|
|
44
|
+
return <FormHelperText ref={pRef} error {...formHelperTextProps} />
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type FormErrors<Values> = FormikErrors<
|
|
48
|
+
Omit<Values, "non_field_errors"> & { non_field_errors: string }
|
|
49
|
+
>
|
|
50
|
+
|
|
51
|
+
type _FormikProps<Values> = Omit<FormikProps<Values>, "errors"> & {
|
|
52
|
+
errors: FormErrors<Values>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type BaseFormProps<Values> = Omit<FormikConfig<Values>, "children"> & {
|
|
56
|
+
children: ReactNode | ((props: _FormikProps<Values>) => ReactNode)
|
|
57
|
+
scrollIntoViewOptions?: ScrollIntoViewOptions
|
|
58
|
+
nonFieldErrorsProps?: Omit<NonFieldErrorsProps, "children">
|
|
59
|
+
order?: Array<{ name: string; inputRef: RefObject<HTMLInputElement> }>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const BaseForm = <Values extends FormValues>({
|
|
16
63
|
children,
|
|
64
|
+
scrollIntoViewOptions = SCROLL_INTO_VIEW_OPTIONS,
|
|
65
|
+
nonFieldErrorsProps,
|
|
66
|
+
order,
|
|
17
67
|
...otherFormikProps
|
|
18
|
-
}:
|
|
68
|
+
}: BaseFormProps<Values>) => (
|
|
19
69
|
<Formik {...otherFormikProps}>
|
|
20
|
-
{
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
70
|
+
{/* @ts-expect-error */}
|
|
71
|
+
{(formik: _FormikProps<Values>) => {
|
|
72
|
+
let nonFieldErrors: undefined | JSX.Element = undefined
|
|
73
|
+
if (Object.keys(formik.errors).length) {
|
|
74
|
+
if (typeof formik.errors.non_field_errors === "string") {
|
|
75
|
+
nonFieldErrors = (
|
|
76
|
+
<NonFieldErrors {...nonFieldErrorsProps}>
|
|
77
|
+
{formik.errors.non_field_errors}
|
|
78
|
+
</NonFieldErrors>
|
|
79
|
+
)
|
|
80
|
+
} else if (order && order.length) {
|
|
81
|
+
const errorNames = getKeyPaths(formik.errors)
|
|
82
|
+
|
|
83
|
+
const inputRef = order.find(({ name }) =>
|
|
84
|
+
errorNames.includes(name),
|
|
85
|
+
)?.inputRef
|
|
86
|
+
|
|
87
|
+
if (inputRef?.current) {
|
|
88
|
+
inputRef.current.scrollIntoView(scrollIntoViewOptions)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<>
|
|
95
|
+
{nonFieldErrors}
|
|
96
|
+
<FormikForm>
|
|
97
|
+
{typeof children === "function" ? children(formik) : children}
|
|
98
|
+
</FormikForm>
|
|
99
|
+
</>
|
|
100
|
+
)
|
|
101
|
+
}}
|
|
25
102
|
</Formik>
|
|
26
103
|
)
|
|
27
104
|
|
|
@@ -29,7 +106,7 @@ type SubmitFormProps<
|
|
|
29
106
|
Values extends FormValues,
|
|
30
107
|
QueryArg extends FormValues,
|
|
31
108
|
ResultType,
|
|
32
|
-
> = Omit<
|
|
109
|
+
> = Omit<BaseFormProps<Values>, "onSubmit"> & {
|
|
33
110
|
useMutation: TypedUseMutation<ResultType, QueryArg, any>
|
|
34
111
|
} & (Values extends QueryArg
|
|
35
112
|
? { submitOptions?: SubmitFormOptions<Values, QueryArg, ResultType> }
|
|
@@ -42,15 +119,16 @@ const SubmitForm = <
|
|
|
42
119
|
>({
|
|
43
120
|
useMutation,
|
|
44
121
|
submitOptions,
|
|
45
|
-
...
|
|
122
|
+
...baseFormProps
|
|
46
123
|
}: SubmitFormProps<Values, QueryArg, ResultType>): JSX.Element => {
|
|
47
124
|
const [trigger] = useMutation()
|
|
48
125
|
|
|
49
126
|
return (
|
|
50
|
-
<
|
|
51
|
-
{...
|
|
127
|
+
<BaseForm
|
|
128
|
+
{...baseFormProps}
|
|
52
129
|
onSubmit={submitForm<Values, QueryArg, ResultType>(
|
|
53
130
|
trigger,
|
|
131
|
+
baseFormProps.initialValues,
|
|
54
132
|
submitOptions as SubmitFormOptions<Values, QueryArg, ResultType>,
|
|
55
133
|
)}
|
|
56
134
|
/>
|
|
@@ -61,10 +139,10 @@ export type FormProps<
|
|
|
61
139
|
Values extends FormValues,
|
|
62
140
|
QueryArg extends FormValues,
|
|
63
141
|
ResultType,
|
|
64
|
-
> =
|
|
142
|
+
> = BaseFormProps<Values> | SubmitFormProps<Values, QueryArg, ResultType>
|
|
65
143
|
|
|
66
144
|
const Form: {
|
|
67
|
-
<Values extends FormValues>(props:
|
|
145
|
+
<Values extends FormValues>(props: BaseFormProps<Values>): JSX.Element
|
|
68
146
|
<Values extends FormValues, QueryArg extends FormValues, ResultType>(
|
|
69
147
|
props: SubmitFormProps<Values, QueryArg, ResultType>,
|
|
70
148
|
): JSX.Element
|
|
@@ -75,8 +153,7 @@ const Form: {
|
|
|
75
153
|
>(
|
|
76
154
|
props: FormProps<Values, QueryArg, ResultType>,
|
|
77
155
|
): JSX.Element => {
|
|
78
|
-
return "onSubmit" in props ? <
|
|
156
|
+
return "onSubmit" in props ? <BaseForm {...props} /> : SubmitForm(props)
|
|
79
157
|
}
|
|
80
158
|
|
|
81
159
|
export default Form
|
|
82
|
-
export { type FormikErrors as FormErrors }
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useRef } from "react"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shorthand for a reference to a HTML input element since this is so common for
|
|
5
|
+
* forms.
|
|
6
|
+
*
|
|
7
|
+
* @returns Ref object to a HTML input element.
|
|
8
|
+
*/
|
|
9
|
+
export function useInputRef() {
|
|
10
|
+
return useRef<HTMLInputElement>(null)
|
|
11
|
+
}
|
package/src/hooks/index.ts
CHANGED
package/src/utils/api.tsx
CHANGED
|
@@ -325,3 +325,8 @@ export function handleResultState<QueryArg, ResultType>(
|
|
|
325
325
|
// Have yet to call the API.
|
|
326
326
|
return loadingNode
|
|
327
327
|
}
|
|
328
|
+
|
|
329
|
+
export function isSafeHttpMethod(method: string) {
|
|
330
|
+
// https://datatracker.ietf.org/doc/html/rfc9110.html#section-9.2.1
|
|
331
|
+
return ["GET", "HEAD", "OPTIONS", "TRACE"].includes(method.toUpperCase())
|
|
332
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { isDirty, getDirty, getCleanNames } from "./form"
|
|
2
|
+
|
|
3
|
+
const VALUES = {
|
|
4
|
+
name: { first: "Peter", last: "Parker" },
|
|
5
|
+
}
|
|
6
|
+
const INITIAL_VALUES: typeof VALUES = {
|
|
7
|
+
name: { first: "Peter", last: "Robbins" },
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// isDirty
|
|
11
|
+
|
|
12
|
+
test("value is dirty", () => {
|
|
13
|
+
const value = isDirty(VALUES, INITIAL_VALUES, "name.last")
|
|
14
|
+
|
|
15
|
+
expect(value).toBe(true)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test("value is clean", () => {
|
|
19
|
+
const value = isDirty(VALUES, INITIAL_VALUES, "name.first")
|
|
20
|
+
|
|
21
|
+
expect(value).toBe(false)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// getDirty
|
|
25
|
+
|
|
26
|
+
test("get dirty values", () => {
|
|
27
|
+
const dirty = getDirty(VALUES, INITIAL_VALUES)
|
|
28
|
+
|
|
29
|
+
expect(dirty).toMatchObject({ "name.first": false, "name.last": true })
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test("get subset of dirty values", () => {
|
|
33
|
+
const dirty = getDirty(VALUES, INITIAL_VALUES, ["name.first"])
|
|
34
|
+
|
|
35
|
+
expect(dirty).toMatchObject({ "name.first": false })
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
// getCleanNames
|
|
39
|
+
|
|
40
|
+
test("get clean names", () => {
|
|
41
|
+
const cleanNames = getCleanNames(VALUES, INITIAL_VALUES)
|
|
42
|
+
|
|
43
|
+
expect(cleanNames).toMatchObject(["name.first"])
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test("get subset of clean names", () => {
|
|
47
|
+
const cleanNames = getCleanNames(VALUES, INITIAL_VALUES, ["name.last"])
|
|
48
|
+
|
|
49
|
+
expect(cleanNames).toMatchObject([])
|
|
50
|
+
})
|
package/src/utils/form.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { TypedMutationTrigger } from "@reduxjs/toolkit/query/react"
|
|
|
2
2
|
import type { FieldValidator, FormikHelpers } from "formik"
|
|
3
3
|
import { ValidationError, type Schema, type ValidateOptions } from "yup"
|
|
4
4
|
|
|
5
|
-
import { excludeKeyPaths } from "./general"
|
|
5
|
+
import { excludeKeyPaths, getNestedProperty, getKeyPaths } from "./general"
|
|
6
6
|
|
|
7
7
|
export type FormValues = Record<string, any>
|
|
8
8
|
|
|
@@ -40,6 +40,8 @@ export type SubmitFormOptions<
|
|
|
40
40
|
ResultType,
|
|
41
41
|
> = Partial<{
|
|
42
42
|
exclude: string[]
|
|
43
|
+
include: string[]
|
|
44
|
+
onlyDirtyValues: boolean
|
|
43
45
|
then: (
|
|
44
46
|
result: ResultType,
|
|
45
47
|
values: Values,
|
|
@@ -67,6 +69,7 @@ export function submitForm<
|
|
|
67
69
|
ResultType,
|
|
68
70
|
>(
|
|
69
71
|
trigger: TypedMutationTrigger<ResultType, QueryArg, any>,
|
|
72
|
+
initialValues: Values,
|
|
70
73
|
options?: SubmitFormOptions<Values, QueryArg, ResultType>,
|
|
71
74
|
): SubmitFormHandler<Values>
|
|
72
75
|
|
|
@@ -76,6 +79,7 @@ export function submitForm<
|
|
|
76
79
|
ResultType,
|
|
77
80
|
>(
|
|
78
81
|
trigger: TypedMutationTrigger<ResultType, QueryArg, any>,
|
|
82
|
+
initialValues: Values,
|
|
79
83
|
options: SubmitFormOptions<Values, QueryArg, ResultType>,
|
|
80
84
|
): SubmitFormHandler<Values>
|
|
81
85
|
|
|
@@ -85,9 +89,17 @@ export function submitForm<
|
|
|
85
89
|
ResultType,
|
|
86
90
|
>(
|
|
87
91
|
trigger: TypedMutationTrigger<ResultType, QueryArg, any>,
|
|
92
|
+
initialValues: Values,
|
|
88
93
|
options?: SubmitFormOptions<Values, QueryArg, ResultType>,
|
|
89
94
|
): SubmitFormHandler<Values> {
|
|
90
|
-
|
|
95
|
+
let {
|
|
96
|
+
exclude = [],
|
|
97
|
+
include,
|
|
98
|
+
onlyDirtyValues = false,
|
|
99
|
+
then,
|
|
100
|
+
catch: _catch,
|
|
101
|
+
finally: _finally,
|
|
102
|
+
} = options || {}
|
|
91
103
|
|
|
92
104
|
return (values, helpers) => {
|
|
93
105
|
let arg =
|
|
@@ -95,7 +107,18 @@ export function submitForm<
|
|
|
95
107
|
? options.clean(values as QueryArg & FormValues)
|
|
96
108
|
: (values as unknown as QueryArg)
|
|
97
109
|
|
|
98
|
-
if (
|
|
110
|
+
if (onlyDirtyValues) {
|
|
111
|
+
exclude = [
|
|
112
|
+
...exclude,
|
|
113
|
+
...getCleanNames(values, initialValues).filter(
|
|
114
|
+
cleanName => !exclude.includes(cleanName),
|
|
115
|
+
),
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (include) exclude = exclude.filter(name => !include.includes(name))
|
|
120
|
+
|
|
121
|
+
if (exclude.length) arg = excludeKeyPaths(arg, exclude)
|
|
99
122
|
|
|
100
123
|
trigger(arg)
|
|
101
124
|
.unwrap()
|
|
@@ -131,23 +154,35 @@ export function schemaToFieldValidator(
|
|
|
131
154
|
|
|
132
155
|
// Checking if individual fields are dirty is not currently supported.
|
|
133
156
|
// https://github.com/jaredpalmer/formik/issues/1421
|
|
134
|
-
export function getDirty<
|
|
135
|
-
Values extends FormValues,
|
|
136
|
-
Names extends Array<keyof Values>,
|
|
137
|
-
>(
|
|
157
|
+
export function getDirty<Values extends FormValues>(
|
|
138
158
|
values: Values,
|
|
139
159
|
initialValues: Values,
|
|
140
|
-
names
|
|
141
|
-
): Record<
|
|
160
|
+
names?: string[],
|
|
161
|
+
): Record<string, boolean> {
|
|
162
|
+
if (!names) names = getKeyPaths(values)
|
|
163
|
+
|
|
142
164
|
return Object.fromEntries(
|
|
143
165
|
names.map(name => [name, isDirty(values, initialValues, name)]),
|
|
144
|
-
)
|
|
166
|
+
)
|
|
145
167
|
}
|
|
146
168
|
|
|
147
|
-
export function isDirty<Values extends FormValues
|
|
169
|
+
export function isDirty<Values extends FormValues>(
|
|
148
170
|
values: Values,
|
|
149
171
|
initialValues: Values,
|
|
150
|
-
name:
|
|
172
|
+
name: string,
|
|
151
173
|
): boolean {
|
|
152
|
-
|
|
174
|
+
const value = getNestedProperty(values, name)
|
|
175
|
+
const initialValue = getNestedProperty(initialValues, name)
|
|
176
|
+
|
|
177
|
+
return value !== initialValue
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function getCleanNames<Values extends FormValues>(
|
|
181
|
+
values: Values,
|
|
182
|
+
initialValues: Values,
|
|
183
|
+
names?: string[],
|
|
184
|
+
): string[] {
|
|
185
|
+
return Object.entries(getDirty(values, initialValues, names))
|
|
186
|
+
.filter(([_, isDirty]) => !isDirty)
|
|
187
|
+
.map(([name]) => name)
|
|
153
188
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
excludeKeyPaths,
|
|
3
|
+
getNestedProperty,
|
|
4
|
+
withKeyPaths,
|
|
5
|
+
getKeyPaths,
|
|
6
|
+
} from "./general"
|
|
2
7
|
|
|
3
8
|
// getNestedProperty
|
|
4
9
|
|
|
@@ -24,12 +29,20 @@ test("get a nested property that doesn't exist", () => {
|
|
|
24
29
|
|
|
25
30
|
// withKeyPaths
|
|
26
31
|
|
|
27
|
-
test("
|
|
32
|
+
test("set the paths of nested keys", () => {
|
|
28
33
|
const obj = withKeyPaths({ a: 1, b: { c: 2, d: { e: 3 } } })
|
|
29
34
|
|
|
30
35
|
expect(obj).toMatchObject({ a: 1, b: { "b.c": 2, "b.d": { "b.d.e": 3 } } })
|
|
31
36
|
})
|
|
32
37
|
|
|
38
|
+
// getKeyPaths
|
|
39
|
+
|
|
40
|
+
test("get the paths of nested keys", () => {
|
|
41
|
+
const keyPaths = getKeyPaths({ a: 1, b: { c: 2, d: { e: 3 } } })
|
|
42
|
+
|
|
43
|
+
expect(keyPaths).toMatchObject(["a", "b", "b.c", "b.d", "b.d.e"])
|
|
44
|
+
})
|
|
45
|
+
|
|
33
46
|
// excludeKeyPaths
|
|
34
47
|
|
|
35
48
|
test("exclude nested keys by their path", () => {
|
package/src/utils/general.ts
CHANGED
|
@@ -966,7 +966,8 @@ export function withKeyPaths(obj: object, delimiter: string = "."): object {
|
|
|
966
966
|
Object.entries(obj).map(([key, value]) => {
|
|
967
967
|
const _path = [...path, key]
|
|
968
968
|
|
|
969
|
-
if (typeof value === "object"
|
|
969
|
+
if (typeof value === "object" && value !== null)
|
|
970
|
+
value = _withKeyPaths(value, _path)
|
|
970
971
|
|
|
971
972
|
return [_path.join(delimiter), value]
|
|
972
973
|
}),
|
|
@@ -976,6 +977,23 @@ export function withKeyPaths(obj: object, delimiter: string = "."): object {
|
|
|
976
977
|
return _withKeyPaths(obj, [])
|
|
977
978
|
}
|
|
978
979
|
|
|
980
|
+
export function getKeyPaths(obj: object, delimiter: string = "."): string[] {
|
|
981
|
+
function _getKeyPaths(obj: object, path: string[]): string[] {
|
|
982
|
+
return Object.entries(obj)
|
|
983
|
+
.map(([key, value]) => {
|
|
984
|
+
const _path = [...path, key]
|
|
985
|
+
const keyPath = _path.join(delimiter)
|
|
986
|
+
|
|
987
|
+
return typeof value === "object" && value !== null
|
|
988
|
+
? [keyPath, ..._getKeyPaths(value, _path)]
|
|
989
|
+
: [keyPath]
|
|
990
|
+
})
|
|
991
|
+
.flat()
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
return _getKeyPaths(obj, [])
|
|
995
|
+
}
|
|
996
|
+
|
|
979
997
|
export function excludeKeyPaths(
|
|
980
998
|
obj: object,
|
|
981
999
|
exclude: string[],
|