mirror of
https://github.com/LukeHagar/form.git
synced 2025-12-06 04:19:43 +00:00
fix: router + store
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { FieldApi, createFormFactory } from "@tanstack/react-form";
|
||||
import {
|
||||
FieldApi,
|
||||
FormApi,
|
||||
createFormFactory,
|
||||
useField,
|
||||
} from "@tanstack/react-form";
|
||||
|
||||
type Person = {
|
||||
firstName: string;
|
||||
@@ -35,7 +40,7 @@ function FieldInfo({ field }: { field: FieldApi<any, any> }) {
|
||||
|
||||
export default function App() {
|
||||
const form = formFactory.useForm({
|
||||
onSubmit: async (values) => {
|
||||
onSubmit: async (values, formApi) => {
|
||||
// Do something with form data
|
||||
console.log(values);
|
||||
},
|
||||
@@ -54,11 +59,14 @@ export default function App() {
|
||||
{/* A type-safe and pre-bound field component*/}
|
||||
<form.Field
|
||||
name="firstName"
|
||||
validateOn="change"
|
||||
validate={(value) => !value && "A first name is required"}
|
||||
validateAsyncOn="change"
|
||||
validateAsyncDebounceMs={500}
|
||||
validateAsync={async (value) => {
|
||||
onChange={(value) =>
|
||||
!value
|
||||
? "A first name is required"
|
||||
: value.length < 3
|
||||
? "First name must be at least 3 characters"
|
||||
: undefined
|
||||
}
|
||||
onChangeAsync={async (value) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return (
|
||||
value.includes("error") && 'No "error" allowed in first name'
|
||||
@@ -68,7 +76,6 @@ export default function App() {
|
||||
// Avoid hasty abstractions. Render props are great!
|
||||
return (
|
||||
<>
|
||||
<input placeholder="uncontrolled" />
|
||||
<input {...field.getInputProps()} />
|
||||
<FieldInfo field={field} />
|
||||
</>
|
||||
@@ -76,7 +83,7 @@ export default function App() {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{/* <div>
|
||||
<form.Field
|
||||
name="lastName"
|
||||
children={(field) => (
|
||||
@@ -172,7 +179,7 @@ export default function App() {
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div> */}
|
||||
<form.Subscribe
|
||||
selector={(state) => [state.canSubmit, state.isSubmitting]}
|
||||
children={([canSubmit, isSubmitting]) => (
|
||||
@@ -188,4 +195,5 @@ export default function App() {
|
||||
}
|
||||
|
||||
const rootElement = document.getElementById("root")!;
|
||||
|
||||
ReactDOM.createRoot(rootElement).render(<App />);
|
||||
|
||||
@@ -116,6 +116,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/store": "^0.0.1-beta.84",
|
||||
"fs-extra": "^11.1.1",
|
||||
"rollup-plugin-dts": "^5.3.0"
|
||||
}
|
||||
|
||||
@@ -27,6 +27,6 @@
|
||||
"build:types": "tsc --build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/store": "0.0.1-beta.84"
|
||||
"@tanstack/store": "0.0.1-beta.88"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,22 +5,30 @@ import { Store } from '@tanstack/store'
|
||||
|
||||
export type ValidationCause = 'change' | 'blur' | 'submit'
|
||||
|
||||
type ValidateFn<TData, TFormData> = (
|
||||
value: TData,
|
||||
fieldApi: FieldApi<TData, TFormData>,
|
||||
) => ValidationError
|
||||
|
||||
type ValidateAsyncFn<TData, TFormData> = (
|
||||
value: TData,
|
||||
fieldApi: FieldApi<TData, TFormData>,
|
||||
) => ValidationError | Promise<ValidationError>
|
||||
|
||||
export interface FieldOptions<TData, TFormData> {
|
||||
name: unknown extends TFormData ? string : DeepKeys<TFormData>
|
||||
index?: TData extends any[] ? number : never
|
||||
defaultValue?: TData
|
||||
validate?: (
|
||||
value: TData,
|
||||
fieldApi: FieldApi<TData, TFormData>,
|
||||
) => ValidationError
|
||||
validateAsync?: (
|
||||
value: TData,
|
||||
fieldApi: FieldApi<TData, TFormData>,
|
||||
) => ValidationError | Promise<ValidationError>
|
||||
validatePristine?: boolean // Default: false
|
||||
validateOn?: ValidationCause // Default: 'change'
|
||||
validateAsyncOn?: ValidationCause // Default: 'blur'
|
||||
validateAsyncDebounceMs?: number
|
||||
asyncDebounceMs?: number
|
||||
asyncAlways?: boolean
|
||||
onMount?: (formApi: FieldApi<TData, TFormData>) => void
|
||||
onChange?: ValidateFn<TData, TFormData>
|
||||
onChangeAsync?: ValidateAsyncFn<TData, TFormData>
|
||||
onChangeAsyncDebounceMs?: number
|
||||
onBlur?: ValidateFn<TData, TFormData>
|
||||
onBlurAsync?: ValidateAsyncFn<TData, TFormData>
|
||||
onBlurAsyncDebounceMs?: number
|
||||
onSubmitAsync?: ValidateAsyncFn<TData, TFormData>
|
||||
defaultMeta?: Partial<FieldMeta>
|
||||
}
|
||||
|
||||
@@ -73,13 +81,7 @@ export class FieldApi<TData, TFormData> {
|
||||
name!: DeepKeys<TFormData>
|
||||
store!: Store<FieldState<TData>>
|
||||
state!: FieldState<TData>
|
||||
options: RequiredByKey<
|
||||
FieldOptions<TData, TFormData>,
|
||||
| 'validatePristine'
|
||||
| 'validateOn'
|
||||
| 'validateAsyncOn'
|
||||
| 'validateAsyncDebounceMs'
|
||||
> = {} as any
|
||||
options: FieldOptions<TData, TFormData> = {} as any
|
||||
|
||||
constructor(opts: FieldApiOptions<TData, TFormData>) {
|
||||
this.form = opts.form
|
||||
@@ -108,12 +110,14 @@ export class FieldApi<TData, TFormData> {
|
||||
? next.meta.error
|
||||
: undefined
|
||||
|
||||
// Do not validate pristine fields
|
||||
const prevState = this.state
|
||||
const prev = this.state
|
||||
this.state = next
|
||||
if (next.value !== prevState.value) {
|
||||
|
||||
if (next.value !== prev.value) {
|
||||
this.validate('change', next.value)
|
||||
}
|
||||
|
||||
return this.state
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -126,10 +130,23 @@ export class FieldApi<TData, TFormData> {
|
||||
const info = this.getInfo()
|
||||
info.instances[this.uid] = this
|
||||
|
||||
const unsubscribe = this.form.store.subscribe(() => {
|
||||
this.#updateStore()
|
||||
const unsubscribe = this.form.store.subscribe((next) => {
|
||||
this.store.batch(() => {
|
||||
const nextValue = this.getValue()
|
||||
const nextMeta = this.getMeta()
|
||||
|
||||
if (nextValue !== this.state.value) {
|
||||
this.store.setState((prev) => ({ ...prev, value: nextValue }))
|
||||
}
|
||||
|
||||
if (nextMeta !== this.state.meta) {
|
||||
this.store.setState((prev) => ({ ...prev, meta: nextMeta }))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.options.onMount?.(this)
|
||||
|
||||
return () => {
|
||||
unsubscribe()
|
||||
delete info.instances[this.uid]
|
||||
@@ -139,28 +156,11 @@ export class FieldApi<TData, TFormData> {
|
||||
}
|
||||
}
|
||||
|
||||
#updateStore = () => {
|
||||
this.store.batch(() => {
|
||||
const nextValue = this.getValue()
|
||||
const nextMeta = this.getMeta()
|
||||
|
||||
if (nextValue !== this.state.value) {
|
||||
this.store.setState((prev) => ({ ...prev, value: nextValue }))
|
||||
}
|
||||
|
||||
if (nextMeta !== this.state.meta) {
|
||||
this.store.setState((prev) => ({ ...prev, meta: nextMeta }))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
update = (opts: FieldApiOptions<TData, TFormData>) => {
|
||||
this.options = {
|
||||
validatePristine: this.form.options.defaultValidatePristine ?? false,
|
||||
validateOn: this.form.options.defaultValidateOn ?? 'change',
|
||||
validateAsyncOn: this.form.options.defaultValidateAsyncOn ?? 'blur',
|
||||
validateAsyncDebounceMs:
|
||||
this.form.options.defaultValidateAsyncDebounceMs ?? 0,
|
||||
asyncDebounceMs: this.form.options.asyncDebounceMs ?? 0,
|
||||
onChangeAsyncDebounceMs: this.form.options.onChangeAsyncDebounceMs ?? 0,
|
||||
onBlurAsyncDebounceMs: this.form.options.onBlurAsyncDebounceMs ?? 0,
|
||||
...opts,
|
||||
}
|
||||
|
||||
@@ -207,12 +207,12 @@ export class FieldApi<TData, TFormData> {
|
||||
form: this.form,
|
||||
})
|
||||
|
||||
validateSync = async (value = this.state.value) => {
|
||||
const { validate } = this.options
|
||||
validateSync = async (value = this.state.value, cause: ValidationCause) => {
|
||||
const { onChange, onBlur } = this.options
|
||||
const validate =
|
||||
cause === 'submit' ? undefined : cause === 'change' ? onChange : onBlur
|
||||
|
||||
if (!validate) {
|
||||
return
|
||||
}
|
||||
if (!validate) return
|
||||
|
||||
// Use the validationCount for all field instances to
|
||||
// track freshness of the validation
|
||||
@@ -249,12 +249,33 @@ export class FieldApi<TData, TFormData> {
|
||||
}))
|
||||
}
|
||||
|
||||
validateAsync = async (value = this.state.value) => {
|
||||
const { validateAsync, validateAsyncDebounceMs } = this.options
|
||||
validateAsync = async (value = this.state.value, cause: ValidationCause) => {
|
||||
const {
|
||||
onChangeAsync,
|
||||
onBlurAsync,
|
||||
onSubmitAsync,
|
||||
asyncDebounceMs,
|
||||
onBlurAsyncDebounceMs,
|
||||
onChangeAsyncDebounceMs,
|
||||
} = this.options
|
||||
|
||||
if (!validateAsync) {
|
||||
return
|
||||
}
|
||||
const validate =
|
||||
cause === 'change'
|
||||
? onChangeAsync
|
||||
: cause === 'submit'
|
||||
? onSubmitAsync
|
||||
: onBlurAsync
|
||||
|
||||
if (!validate) return
|
||||
|
||||
const debounceMs =
|
||||
cause === 'submit'
|
||||
? 0
|
||||
: (cause === 'change'
|
||||
? onChangeAsyncDebounceMs
|
||||
: onBlurAsyncDebounceMs) ??
|
||||
asyncDebounceMs ??
|
||||
500
|
||||
|
||||
if (this.state.meta.isValidating !== true)
|
||||
this.setMeta((prev) => ({ ...prev, isValidating: true }))
|
||||
@@ -273,14 +294,14 @@ export class FieldApi<TData, TFormData> {
|
||||
})
|
||||
}
|
||||
|
||||
if (validateAsyncDebounceMs > 0) {
|
||||
await new Promise((r) => setTimeout(r, validateAsyncDebounceMs))
|
||||
if (debounceMs > 0) {
|
||||
await new Promise((r) => setTimeout(r, debounceMs))
|
||||
}
|
||||
|
||||
// Only kick off validation if this validation is the latest attempt
|
||||
if (checkLatest()) {
|
||||
try {
|
||||
const rawError = await validateAsync(value, this)
|
||||
const rawError = await validate(value, this)
|
||||
|
||||
if (checkLatest()) {
|
||||
const error = normalizeError(rawError)
|
||||
@@ -308,44 +329,25 @@ export class FieldApi<TData, TFormData> {
|
||||
return this.getInfo().validationPromise
|
||||
}
|
||||
|
||||
shouldValidate = (isAsync: boolean, cause?: ValidationCause) => {
|
||||
const { validateOn, validateAsyncOn } = this.options
|
||||
const level = getValidationCauseLevel(cause)
|
||||
|
||||
// Must meet *at least* the validation level to validate,
|
||||
// e.g. if validateOn is 'change' and validateCause is 'blur',
|
||||
// the field will still validate
|
||||
return Object.keys(validateCauseLevels).some((d) =>
|
||||
isAsync
|
||||
? validateAsyncOn
|
||||
: validateOn === d && level >= validateCauseLevels[d],
|
||||
)
|
||||
}
|
||||
|
||||
validate = async (
|
||||
cause?: ValidationCause,
|
||||
validate = (
|
||||
cause: ValidationCause,
|
||||
value?: TData,
|
||||
): Promise<ValidationError> => {
|
||||
): ValidationError | Promise<ValidationError> => {
|
||||
// If the field is pristine and validatePristine is false, do not validate
|
||||
if (!this.options.validatePristine && !this.state.meta.isTouched) return
|
||||
if (!this.state.meta.isTouched) return
|
||||
|
||||
// Attempt to sync validate first
|
||||
if (this.shouldValidate(false, cause)) {
|
||||
this.validateSync(value)
|
||||
}
|
||||
this.validateSync(value, cause)
|
||||
|
||||
// If there is an error, return it, do not attempt async validation
|
||||
if (this.state.meta.error) {
|
||||
return this.state.meta.error
|
||||
if (!this.options.asyncAlways) {
|
||||
return this.state.meta.error
|
||||
}
|
||||
}
|
||||
|
||||
// No error? Attempt async validation
|
||||
if (this.shouldValidate(true, cause)) {
|
||||
return this.validateAsync(value)
|
||||
}
|
||||
|
||||
// If there is no sync error or async validation attempt, there is no error
|
||||
return undefined
|
||||
return this.validateAsync(value, cause)
|
||||
}
|
||||
|
||||
getChangeProps = <T extends UserChangeProps<any>>(
|
||||
@@ -359,9 +361,12 @@ export class FieldApi<TData, TFormData> {
|
||||
props.onChange?.(value)
|
||||
},
|
||||
onBlur: (e) => {
|
||||
const prevTouched = this.state.meta.isTouched
|
||||
this.setMeta((prev) => ({ ...prev, isTouched: true }))
|
||||
if (!prevTouched) {
|
||||
this.validate('change')
|
||||
}
|
||||
this.validate('blur')
|
||||
props.onBlur?.(e)
|
||||
},
|
||||
} as ChangeProps<TData> & Omit<T, keyof ChangeProps<TData>>
|
||||
}
|
||||
@@ -381,16 +386,6 @@ export class FieldApi<TData, TFormData> {
|
||||
}
|
||||
}
|
||||
|
||||
const validateCauseLevels = {
|
||||
change: 0,
|
||||
blur: 1,
|
||||
submit: 2,
|
||||
}
|
||||
|
||||
function getValidationCauseLevel(cause?: ValidationCause) {
|
||||
return !cause ? 3 : validateCauseLevels[cause]
|
||||
}
|
||||
|
||||
function normalizeError(rawError?: ValidationError) {
|
||||
if (rawError) {
|
||||
if (typeof rawError !== 'string') {
|
||||
|
||||
@@ -17,13 +17,27 @@ export type FormSubmitEvent = Register extends {
|
||||
export type FormOptions<TData> = {
|
||||
defaultValues?: TData
|
||||
defaultState?: Partial<FormState<TData>>
|
||||
onSubmit?: (values: TData, formApi: FormApi<TData>) => void
|
||||
onInvalidSubmit?: (values: TData, formApi: FormApi<TData>) => void
|
||||
validate?: (values: TData, formApi: FormApi<TData>) => Promise<any>
|
||||
defaultValidatePristine?: boolean
|
||||
defaultValidateOn?: ValidationCause
|
||||
defaultValidateAsyncOn?: ValidationCause
|
||||
defaultValidateAsyncDebounceMs?: number
|
||||
asyncDebounceMs?: number
|
||||
onMount?: (values: TData, formApi: FormApi<TData>) => ValidationError
|
||||
onMountAsync?: (
|
||||
values: TData,
|
||||
formApi: FormApi<TData>,
|
||||
) => ValidationError | Promise<ValidationError>
|
||||
onMountAsyncDebounceMs?: number
|
||||
onChange?: (values: TData, formApi: FormApi<TData>) => ValidationError
|
||||
onChangeAsync?: (
|
||||
values: TData,
|
||||
formApi: FormApi<TData>,
|
||||
) => ValidationError | Promise<ValidationError>
|
||||
onChangeAsyncDebounceMs?: number
|
||||
onBlur?: (values: TData, formApi: FormApi<TData>) => ValidationError
|
||||
onBlurAsync?: (
|
||||
values: TData,
|
||||
formApi: FormApi<TData>,
|
||||
) => ValidationError | Promise<ValidationError>
|
||||
onBlurAsyncDebounceMs?: number
|
||||
onSubmit?: (values: TData, formApi: FormApi<TData>) => any | Promise<any>
|
||||
onSubmitInvalid?: (values: TData, formApi: FormApi<TData>) => void
|
||||
}
|
||||
|
||||
export type FieldInfo<TFormData> = {
|
||||
@@ -99,7 +113,7 @@ export class FormApi<TFormData> {
|
||||
getDefaultFormState({
|
||||
...opts?.defaultState,
|
||||
values: opts?.defaultValues ?? opts?.defaultState?.values,
|
||||
isFormValid: !opts?.validate,
|
||||
isFormValid: true,
|
||||
}),
|
||||
{
|
||||
onUpdate: (next) => {
|
||||
@@ -175,7 +189,7 @@ export class FormApi<TFormData> {
|
||||
reset = () =>
|
||||
this.store.setState(() => getDefaultFormState(this.options.defaultValues!))
|
||||
|
||||
validateAllFields = async () => {
|
||||
validateAllFields = async (cause: ValidationCause) => {
|
||||
const fieldValidationPromises: Promise<ValidationError>[] = [] as any
|
||||
|
||||
this.store.batch(() => {
|
||||
@@ -187,9 +201,9 @@ export class FormApi<TFormData> {
|
||||
// Mark them as touched
|
||||
instance.setMeta((prev) => ({ ...prev, isTouched: true }))
|
||||
// Validate the field
|
||||
if (instance.options.validate) {
|
||||
fieldValidationPromises.push(instance.validate())
|
||||
}
|
||||
fieldValidationPromises.push(
|
||||
Promise.resolve().then(() => instance.validate(cause)),
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -199,63 +213,7 @@ export class FormApi<TFormData> {
|
||||
return Promise.all(fieldValidationPromises)
|
||||
}
|
||||
|
||||
validateForm = async () => {
|
||||
const { validate } = this.options
|
||||
|
||||
if (!validate) {
|
||||
return
|
||||
}
|
||||
|
||||
// Use the formValidationCount for all field instances to
|
||||
// track freshness of the validation
|
||||
this.store.setState((prev) => ({
|
||||
...prev,
|
||||
isValidating: true,
|
||||
formValidationCount: prev.formValidationCount + 1,
|
||||
}))
|
||||
|
||||
const formValidationCount = this.state.formValidationCount
|
||||
|
||||
const checkLatest = () =>
|
||||
formValidationCount === this.state.formValidationCount
|
||||
|
||||
if (!this.validationMeta.validationPromise) {
|
||||
this.validationMeta.validationPromise = new Promise((resolve, reject) => {
|
||||
this.validationMeta.validationResolve = resolve
|
||||
this.validationMeta.validationReject = reject
|
||||
})
|
||||
}
|
||||
|
||||
const doValidation = async () => {
|
||||
try {
|
||||
const error = await validate(this.state.values, this)
|
||||
|
||||
if (checkLatest()) {
|
||||
this.store.setState((prev) => ({
|
||||
...prev,
|
||||
isValidating: false,
|
||||
error: error
|
||||
? typeof error === 'string'
|
||||
? error
|
||||
: 'Invalid Form Values'
|
||||
: null,
|
||||
}))
|
||||
|
||||
this.validationMeta.validationResolve?.(error)
|
||||
}
|
||||
} catch (err) {
|
||||
if (checkLatest()) {
|
||||
this.validationMeta.validationReject?.(err)
|
||||
}
|
||||
} finally {
|
||||
delete this.validationMeta.validationPromise
|
||||
}
|
||||
}
|
||||
|
||||
doValidation()
|
||||
|
||||
return this.validationMeta.validationPromise
|
||||
}
|
||||
validateForm = async () => {}
|
||||
|
||||
handleSubmit = async (e: FormSubmitEvent) => {
|
||||
e.preventDefault()
|
||||
@@ -284,12 +242,12 @@ export class FormApi<TFormData> {
|
||||
}
|
||||
|
||||
// Validate all fields
|
||||
await this.validateAllFields()
|
||||
await this.validateAllFields('submit')
|
||||
|
||||
// Fields are invalid, do not submit
|
||||
if (!this.state.isFieldsValid) {
|
||||
done()
|
||||
this.options.onInvalidSubmit?.(this.state.values, this)
|
||||
this.options.onSubmitInvalid?.(this.state.values, this)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -298,7 +256,7 @@ export class FormApi<TFormData> {
|
||||
|
||||
if (!this.state.isValid) {
|
||||
done()
|
||||
this.options.onInvalidSubmit?.(this.state.values, this)
|
||||
this.options.onSubmitInvalid?.(this.state.values, this)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -352,22 +310,22 @@ export class FormApi<TFormData> {
|
||||
updater: Updater<DeepValue<TFormData, TField>>,
|
||||
opts?: { touch?: boolean },
|
||||
) => {
|
||||
const touch = opts?.touch ?? true
|
||||
const touch = opts?.touch
|
||||
|
||||
this.store.batch(() => {
|
||||
this.store.setState((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
values: setBy(prev.values, field, updater),
|
||||
}
|
||||
})
|
||||
|
||||
if (touch) {
|
||||
this.setFieldMeta(field, (prev) => ({
|
||||
...prev,
|
||||
isTouched: true,
|
||||
}))
|
||||
}
|
||||
|
||||
this.store.setState((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
values: setBy(prev.values, field, updater),
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -392,10 +350,6 @@ export class FormApi<TFormData> {
|
||||
this.setFieldValue(
|
||||
field,
|
||||
(prev) => {
|
||||
// invariant( // TODO: bring in invariant
|
||||
// Array.isArray(prev),
|
||||
// `Cannot insert a field value into a non-array field. Check that this field's existing value is an array: ${field}.`
|
||||
// )
|
||||
return (prev as DeepValue<TFormData, TField>[]).map((d, i) =>
|
||||
i === index ? value : d,
|
||||
) as any
|
||||
@@ -412,10 +366,6 @@ export class FormApi<TFormData> {
|
||||
this.setFieldValue(
|
||||
field,
|
||||
(prev) => {
|
||||
// invariant( // TODO: bring in invariant
|
||||
// Array.isArray(prev),
|
||||
// `Cannot insert a field value into a non-array field. Check that this field's existing value is an array: ${field}.`
|
||||
// )
|
||||
return (prev as DeepValue<TFormData, TField>[]).filter(
|
||||
(_d, i) => i !== index,
|
||||
) as any
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/form-core": "workspace:*",
|
||||
"@tanstack/react-store": "0.0.1-beta.84"
|
||||
"@tanstack/react-store": "0.0.1-beta.85"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.0 || ^18.0.0",
|
||||
|
||||
3676
pnpm-lock.yaml
generated
3676
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user