mirror of
https://github.com/nowcommunity/nowdatabase.git
synced 2026-10-12 19:13:52 +03:00
Fix reference full-field validation
This commit is contained in:
@@ -135,6 +135,7 @@ export type DetailContextType<T> = {
|
||||
optionalRadioSelectionProps?: OptionalRadioSelectionProps
|
||||
) => JSX.Element
|
||||
validator: (editData: EditDataType<T>, field: keyof EditDataType<T>) => ValidationObject
|
||||
validateFields?: (editData: EditDataType<T>) => ValidationObject[]
|
||||
fieldsWithErrors: FieldsWithErrorsType
|
||||
setFieldsWithErrors: SetFieldsWithErrorsType
|
||||
isDirty: boolean
|
||||
@@ -211,6 +212,9 @@ export const DetailContextProvider = <T extends object>({
|
||||
markEditDataClean,
|
||||
validator: (editData: unknown, fieldName: keyof EditDataType<T>) =>
|
||||
contextState.validator(editData as EditDataType<T>, fieldName),
|
||||
validateFields: contextState.validateFields
|
||||
? (editData: unknown) => contextState.validateFields?.(editData as EditDataType<T>) ?? []
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -108,6 +108,7 @@ export const DetailView = <T extends object>({
|
||||
data,
|
||||
onWrite,
|
||||
validator,
|
||||
validateFields,
|
||||
isNew = false,
|
||||
isUserPage = false,
|
||||
isPersonPage = false,
|
||||
@@ -124,6 +125,7 @@ export const DetailView = <T extends object>({
|
||||
markEditDataClean?: (editData?: EditDataType<T>) => void
|
||||
) => Promise<WriteResult>
|
||||
validator: (editData: EditDataType<T>, field: keyof EditDataType<T>) => ValidationObject
|
||||
validateFields?: (editData: EditDataType<T>) => ValidationObject[]
|
||||
isNew?: boolean
|
||||
isUserPage?: boolean
|
||||
isPersonPage?: boolean
|
||||
@@ -193,6 +195,7 @@ export const DetailView = <T extends object>({
|
||||
radioSelection,
|
||||
bigTextField,
|
||||
validator,
|
||||
validateFields,
|
||||
fieldsWithErrors,
|
||||
setFieldsWithErrors,
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import type { DropdownOption } from './common/editingComponents'
|
||||
import type { FieldsWithErrorsType, OptionalRadioSelectionProps, TextFieldOptions } from './DetailView'
|
||||
import type { ValidationObject } from '@/shared/validators/validator'
|
||||
import {
|
||||
createReferenceFieldsValidatorWithLabels,
|
||||
createReferenceValidatorWithLabels,
|
||||
ReferenceDisplayLabelMap,
|
||||
ReferenceFieldDisplayNames,
|
||||
@@ -32,16 +33,13 @@ import { formatReferenceValidationErrorMessage } from '@/components/Reference/re
|
||||
const NewReferenceDialogContent = ({
|
||||
onClose,
|
||||
onCreated,
|
||||
referenceValidator,
|
||||
validateReferenceFields,
|
||||
referenceFieldDisplayLabelMap,
|
||||
referenceTypes,
|
||||
}: {
|
||||
onClose: () => void
|
||||
onCreated: (reference: ReferenceDetailsType) => void
|
||||
referenceValidator: (
|
||||
editData: EditDataType<ReferenceDetailsType>,
|
||||
field: keyof EditDataType<ReferenceDetailsType>
|
||||
) => ValidationObject
|
||||
validateReferenceFields: (editData: EditDataType<ReferenceDetailsType>) => ValidationObject[]
|
||||
referenceFieldDisplayLabelMap?: ReferenceDisplayLabelMap
|
||||
referenceTypes?: ReferenceType[]
|
||||
}) => {
|
||||
@@ -52,12 +50,8 @@ const NewReferenceDialogContent = ({
|
||||
const validateAllFields = () => {
|
||||
const nextFieldsWithErrors: FieldsWithErrorsType = {}
|
||||
|
||||
for (const field in editData) {
|
||||
const fieldKey = field as keyof EditDataType<ReferenceDetailsType>
|
||||
const errorObject = referenceValidator(editData, fieldKey)
|
||||
if (errorObject.error) {
|
||||
nextFieldsWithErrors[String(fieldKey)] = errorObject
|
||||
}
|
||||
for (const errorObject of validateReferenceFields(editData)) {
|
||||
nextFieldsWithErrors[String(errorObject.name)] = errorObject
|
||||
}
|
||||
|
||||
setFieldsWithErrors(() => nextFieldsWithErrors)
|
||||
@@ -136,6 +130,10 @@ const NewReferenceDialog = ({
|
||||
() => createReferenceValidatorWithLabels(referenceFieldDisplayLabelMap),
|
||||
[referenceFieldDisplayLabelMap]
|
||||
)
|
||||
const validateReferenceFields = useMemo(
|
||||
() => createReferenceFieldsValidatorWithLabels(referenceFieldDisplayLabelMap),
|
||||
[referenceFieldDisplayLabelMap]
|
||||
)
|
||||
|
||||
const textField = (field: keyof EditDataType<ReferenceDetailsType>, options?: TextFieldOptions) => (
|
||||
<EditableTextField<ReferenceDetailsType> field={field} {...options} />
|
||||
@@ -193,6 +191,7 @@ const NewReferenceDialog = ({
|
||||
radioSelection,
|
||||
bigTextField,
|
||||
validator: referenceValidator,
|
||||
validateFields: validateReferenceFields,
|
||||
fieldsWithErrors,
|
||||
setFieldsWithErrors,
|
||||
}}
|
||||
@@ -200,7 +199,7 @@ const NewReferenceDialog = ({
|
||||
<NewReferenceDialogContent
|
||||
onClose={onClose}
|
||||
onCreated={onCreated}
|
||||
referenceValidator={referenceValidator}
|
||||
validateReferenceFields={validateReferenceFields}
|
||||
referenceFieldDisplayLabelMap={referenceFieldDisplayLabelMap}
|
||||
referenceTypes={referenceTypes}
|
||||
/>
|
||||
|
||||
@@ -42,6 +42,7 @@ export const WriteButton = <T,>({
|
||||
mode,
|
||||
setMode,
|
||||
validator,
|
||||
validateFields,
|
||||
fieldsWithErrors,
|
||||
setFieldsWithErrors,
|
||||
isDirty,
|
||||
@@ -108,10 +109,27 @@ export const WriteButton = <T,>({
|
||||
|
||||
useEffect(() => {
|
||||
if (mode.option === 'edit' || mode.option === 'new') {
|
||||
for (const field in editData) {
|
||||
const fieldAsString = String(field)
|
||||
const errorObject = validator(editData, field)
|
||||
checkFieldErrors(fieldAsString, errorObject, fieldsWithErrors, setFieldsWithErrors)
|
||||
if (validateFields) {
|
||||
const validationErrors = validateFields(editData)
|
||||
setFieldsWithErrors(prevFieldsWithErrors => {
|
||||
const nextFieldsWithErrors = { ...prevFieldsWithErrors }
|
||||
|
||||
for (const field of Object.keys(nextFieldsWithErrors)) {
|
||||
if (field !== 'mandatoryReference') delete nextFieldsWithErrors[field]
|
||||
}
|
||||
|
||||
for (const errorObject of validationErrors) {
|
||||
nextFieldsWithErrors[String(errorObject.name)] = errorObject
|
||||
}
|
||||
|
||||
return nextFieldsWithErrors
|
||||
})
|
||||
} else {
|
||||
for (const field in editData) {
|
||||
const fieldAsString = String(field)
|
||||
const errorObject = validator(editData, field)
|
||||
checkFieldErrors(fieldAsString, errorObject, fieldsWithErrors, setFieldsWithErrors)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mode.staging == true) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { EditDataType, ReferenceDetailsType } from '@/shared/types'
|
||||
import { emptyReference } from '../DetailView/common/defaultValues'
|
||||
import {
|
||||
createReferenceFieldsValidatorWithLabels,
|
||||
createReferenceValidatorWithLabels,
|
||||
ReferenceFieldDisplayNames,
|
||||
ReferenceDisplayLabelMap,
|
||||
@@ -82,6 +83,10 @@ export const ReferenceDetails = ({
|
||||
() => createReferenceValidatorWithLabels(referenceFieldDisplayLabelMap),
|
||||
[referenceFieldDisplayLabelMap]
|
||||
)
|
||||
const validateReferenceFields = useMemo(
|
||||
() => createReferenceFieldsValidatorWithLabels(referenceFieldDisplayLabelMap),
|
||||
[referenceFieldDisplayLabelMap]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (deleteSuccess) {
|
||||
@@ -138,6 +143,7 @@ export const ReferenceDetails = ({
|
||||
data={isNew ? emptyReference : data!}
|
||||
onWrite={onWrite}
|
||||
validator={referenceValidator}
|
||||
validateFields={validateReferenceFields}
|
||||
deleteFunction={deleteFunction}
|
||||
wrapWithUnsavedChangesProvider={wrapWithUnsavedChangesProvider}
|
||||
/>
|
||||
|
||||
@@ -8,28 +8,55 @@ import { JournalTab } from './JournalTab'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export const ReferenceTab = () => {
|
||||
const { dropdown, data, editData, mode, textField, bigTextField, fieldsWithErrors, validator, setFieldsWithErrors } =
|
||||
useDetailContext<ReferenceDetailsType>()
|
||||
const {
|
||||
dropdown,
|
||||
data,
|
||||
editData,
|
||||
mode,
|
||||
textField,
|
||||
bigTextField,
|
||||
fieldsWithErrors,
|
||||
validator,
|
||||
validateFields,
|
||||
setFieldsWithErrors,
|
||||
} = useDetailContext<ReferenceDetailsType>()
|
||||
const { data: referenceTypes } = useGetReferenceTypesQuery()
|
||||
|
||||
useEffect(() => {
|
||||
if (mode.option === 'edit' || mode.option === 'new') {
|
||||
for (const field in editData) {
|
||||
const fieldAsKey = field as keyof typeof editData
|
||||
const fieldAsString = String(fieldAsKey)
|
||||
const errorObject = validator(editData, fieldAsKey)
|
||||
if (errorObject.error) {
|
||||
if (!(fieldAsString in fieldsWithErrors)) {
|
||||
if (validateFields) {
|
||||
const validationErrors = validateFields(editData)
|
||||
setFieldsWithErrors(prevFieldsWithErrors => {
|
||||
const nextFieldsWithErrors = { ...prevFieldsWithErrors }
|
||||
|
||||
for (const field of Object.keys(nextFieldsWithErrors)) {
|
||||
if (field !== 'mandatoryReference') delete nextFieldsWithErrors[field]
|
||||
}
|
||||
|
||||
for (const errorObject of validationErrors) {
|
||||
nextFieldsWithErrors[String(errorObject.name)] = errorObject
|
||||
}
|
||||
|
||||
return nextFieldsWithErrors
|
||||
})
|
||||
} else {
|
||||
for (const field in editData) {
|
||||
const fieldAsKey = field as keyof typeof editData
|
||||
const fieldAsString = String(fieldAsKey)
|
||||
const errorObject = validator(editData, fieldAsKey)
|
||||
if (errorObject.error) {
|
||||
if (!(fieldAsString in fieldsWithErrors)) {
|
||||
setFieldsWithErrors(prevFieldsWithErrors => {
|
||||
return { ...prevFieldsWithErrors, [fieldAsString]: errorObject }
|
||||
})
|
||||
}
|
||||
} else if (!errorObject.error && fieldAsString in fieldsWithErrors) {
|
||||
setFieldsWithErrors(prevFieldsWithErrors => {
|
||||
return { ...prevFieldsWithErrors, [fieldAsString]: errorObject }
|
||||
const newFieldsWithErrors = { ...prevFieldsWithErrors }
|
||||
delete newFieldsWithErrors[fieldAsString]
|
||||
return newFieldsWithErrors
|
||||
})
|
||||
}
|
||||
} else if (!errorObject.error && fieldAsString in fieldsWithErrors) {
|
||||
setFieldsWithErrors(prevFieldsWithErrors => {
|
||||
const newFieldsWithErrors = { ...prevFieldsWithErrors }
|
||||
delete newFieldsWithErrors[fieldAsString]
|
||||
return newFieldsWithErrors
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EditDataType, ReferenceDetailsType, ReferenceAuthorType, ReferenceJournalType } from '../types'
|
||||
import { Validators, validator, ValidationError } from './validator'
|
||||
import { Validators, validateFields, validator, ValidationError } from './validator'
|
||||
|
||||
const authorCheck: (data: ReferenceAuthorType[]) => ValidationError = (data: ReferenceAuthorType[]) => {
|
||||
if (data.length === 0) {
|
||||
@@ -173,109 +173,122 @@ const orCheck = (
|
||||
return null
|
||||
}
|
||||
|
||||
const createReferenceValidators = (
|
||||
editData: EditDataType<ReferenceDetailsType>,
|
||||
options?: ReferenceValidationOptions
|
||||
): Validators<Partial<EditDataType<ReferenceDetailsType>>> => ({
|
||||
title_primary: {
|
||||
name: 'title_primary',
|
||||
useEditData: true,
|
||||
miscCheck: (obj: object) => {
|
||||
return orCheck(obj as EditDataType<ReferenceDetailsType>, 'title_primary', options)
|
||||
},
|
||||
},
|
||||
title_secondary: {
|
||||
name: 'title_secondary',
|
||||
useEditData: true,
|
||||
miscCheck: (obj: object) => {
|
||||
return orCheck(obj as EditDataType<ReferenceDetailsType>, 'title_secondary', options)
|
||||
},
|
||||
},
|
||||
title_series: {
|
||||
name: 'title_series',
|
||||
useEditData: true,
|
||||
miscCheck: (obj: object) => {
|
||||
return orCheck(obj as EditDataType<ReferenceDetailsType>, 'title_series', options)
|
||||
},
|
||||
},
|
||||
gen_notes: {
|
||||
name: 'gen_notes',
|
||||
useEditData: true,
|
||||
miscCheck: (obj: object) => {
|
||||
return orCheck(obj as EditDataType<ReferenceDetailsType>, 'gen_notes', options)
|
||||
},
|
||||
},
|
||||
ref_type_id: {
|
||||
name: 'ref_type_id',
|
||||
required: true,
|
||||
asNumber: true,
|
||||
},
|
||||
date_primary: {
|
||||
name: 'date_primary',
|
||||
required: true,
|
||||
asNumber: yearCheck,
|
||||
condition: (data: Partial<EditDataType<ReferenceDetailsType>>) => {
|
||||
const ids: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
|
||||
return data.ref_type_id != null && ids.includes(data.ref_type_id)
|
||||
},
|
||||
},
|
||||
start_page: {
|
||||
name: 'start_page',
|
||||
required: false,
|
||||
asNumber: (num: number) => positiveIntegerCheck('start_page', num),
|
||||
},
|
||||
end_page: {
|
||||
name: 'end_page',
|
||||
required: false,
|
||||
asNumber: (num: number) => {
|
||||
const base = positiveIntegerCheck('end_page', num)
|
||||
if (base) return base
|
||||
if (typeof editData.start_page === 'number' && num < editData.start_page) {
|
||||
return 'end_page must be greater than or equal to start_page'
|
||||
}
|
||||
return null
|
||||
},
|
||||
},
|
||||
date_secondary: {
|
||||
name: 'date_secondary',
|
||||
required: false,
|
||||
asNumber: yearCheck,
|
||||
},
|
||||
ref_authors: {
|
||||
name: 'ref_authors',
|
||||
required: true,
|
||||
minLength: 1,
|
||||
miscArray: authorCheck,
|
||||
condition: (data: Partial<EditDataType<ReferenceDetailsType>>) => {
|
||||
const ids: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
|
||||
return data.ref_type_id != null && ids.includes(data.ref_type_id)
|
||||
},
|
||||
},
|
||||
ref_journal: {
|
||||
name: 'ref_journal',
|
||||
miscCheck: journalCheck,
|
||||
condition: (data: Partial<EditDataType<ReferenceDetailsType>>) => {
|
||||
const ids: number[] = [1, 5, 14]
|
||||
return data.ref_type_id != null && ids.includes(data.ref_type_id)
|
||||
},
|
||||
},
|
||||
exact_date: {
|
||||
name: 'exact_date',
|
||||
required: true,
|
||||
asString: dateCheck,
|
||||
condition: (data: Partial<EditDataType<ReferenceDetailsType>>) => {
|
||||
const ids: number[] = [6, 7, 10, 11, 12, 13, 14]
|
||||
return data.ref_type_id != null && ids.includes(data.ref_type_id)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const validateReference = (
|
||||
editData: EditDataType<ReferenceDetailsType>,
|
||||
fieldName: keyof EditDataType<ReferenceDetailsType>,
|
||||
options?: ReferenceValidationOptions
|
||||
) => {
|
||||
const validators: Validators<Partial<EditDataType<ReferenceDetailsType>>> = {
|
||||
title_primary: {
|
||||
name: 'title_primary',
|
||||
useEditData: true,
|
||||
miscCheck: (obj: object) => {
|
||||
return orCheck(obj as EditDataType<ReferenceDetailsType>, 'title_primary', options)
|
||||
},
|
||||
},
|
||||
title_secondary: {
|
||||
name: 'title_secondary',
|
||||
useEditData: true,
|
||||
miscCheck: (obj: object) => {
|
||||
return orCheck(obj as EditDataType<ReferenceDetailsType>, 'title_secondary', options)
|
||||
},
|
||||
},
|
||||
title_series: {
|
||||
name: 'title_series',
|
||||
useEditData: true,
|
||||
miscCheck: (obj: object) => {
|
||||
return orCheck(obj as EditDataType<ReferenceDetailsType>, 'title_series', options)
|
||||
},
|
||||
},
|
||||
gen_notes: {
|
||||
name: 'gen_notes',
|
||||
useEditData: true,
|
||||
miscCheck: (obj: object) => {
|
||||
return orCheck(obj as EditDataType<ReferenceDetailsType>, 'gen_notes', options)
|
||||
},
|
||||
},
|
||||
ref_type_id: {
|
||||
name: 'ref_type_id',
|
||||
required: true,
|
||||
asNumber: true,
|
||||
},
|
||||
date_primary: {
|
||||
name: 'date_primary',
|
||||
required: true,
|
||||
asNumber: yearCheck,
|
||||
condition: (data: Partial<EditDataType<ReferenceDetailsType>>) => {
|
||||
const ids: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
|
||||
return data.ref_type_id != null && ids.includes(data.ref_type_id)
|
||||
},
|
||||
},
|
||||
start_page: {
|
||||
name: 'start_page',
|
||||
required: false,
|
||||
asNumber: (num: number) => positiveIntegerCheck('start_page', num),
|
||||
},
|
||||
end_page: {
|
||||
name: 'end_page',
|
||||
required: false,
|
||||
asNumber: (num: number) => {
|
||||
const base = positiveIntegerCheck('end_page', num)
|
||||
if (base) return base
|
||||
if (typeof editData.start_page === 'number' && num < editData.start_page) {
|
||||
return 'end_page must be greater than or equal to start_page'
|
||||
}
|
||||
return null
|
||||
},
|
||||
},
|
||||
date_secondary: {
|
||||
name: 'date_secondary',
|
||||
required: false,
|
||||
asNumber: yearCheck,
|
||||
},
|
||||
ref_authors: {
|
||||
name: 'ref_authors',
|
||||
required: true,
|
||||
minLength: 1,
|
||||
miscArray: authorCheck,
|
||||
condition: (data: Partial<EditDataType<ReferenceDetailsType>>) => {
|
||||
const ids: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
|
||||
return data.ref_type_id != null && ids.includes(data.ref_type_id)
|
||||
},
|
||||
},
|
||||
ref_journal: {
|
||||
name: 'ref_journal',
|
||||
miscCheck: journalCheck,
|
||||
condition: (data: Partial<EditDataType<ReferenceDetailsType>>) => {
|
||||
const ids: number[] = [1, 5, 14]
|
||||
return data.ref_type_id != null && ids.includes(data.ref_type_id)
|
||||
},
|
||||
},
|
||||
exact_date: {
|
||||
name: 'exact_date',
|
||||
required: true,
|
||||
asString: dateCheck,
|
||||
condition: (data: Partial<EditDataType<ReferenceDetailsType>>) => {
|
||||
const ids: number[] = [6, 7, 10, 11, 12, 13, 14]
|
||||
return data.ref_type_id != null && ids.includes(data.ref_type_id)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const validators = createReferenceValidators(editData, options)
|
||||
return validator<EditDataType<ReferenceDetailsType>>(validators, editData, fieldName)
|
||||
}
|
||||
|
||||
export const validateReferenceFields = (
|
||||
editData: EditDataType<ReferenceDetailsType>,
|
||||
options?: ReferenceValidationOptions
|
||||
) => validateFields<EditDataType<ReferenceDetailsType>>(createReferenceValidators(editData, options), editData)
|
||||
|
||||
export const createReferenceValidatorWithLabels =
|
||||
(displayLabelMap?: ReferenceDisplayLabelMap) =>
|
||||
(editData: EditDataType<ReferenceDetailsType>, fieldName: keyof EditDataType<ReferenceDetailsType>) =>
|
||||
validateReference(editData, fieldName, { displayLabelMap })
|
||||
|
||||
export const createReferenceFieldsValidatorWithLabels =
|
||||
(displayLabelMap?: ReferenceDisplayLabelMap) => (editData: EditDataType<ReferenceDetailsType>) =>
|
||||
validateReferenceFields(editData, { displayLabelMap })
|
||||
|
||||
@@ -75,6 +75,7 @@ jest.mock('@/components/Reference/Tabs/ReferenceTab', () => ({
|
||||
|
||||
jest.mock('@/shared/validators/reference', () => ({
|
||||
createReferenceValidatorWithLabels: () => (_editData: unknown, field: string) => ({ name: field, error: null }),
|
||||
createReferenceFieldsValidatorWithLabels: () => () => [],
|
||||
}))
|
||||
|
||||
jest.mock('@/hooks/notification', () => ({
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { describe, expect, it, jest } from '@jest/globals'
|
||||
|
||||
import { createReferenceValidatorWithLabels } from '@/shared/validators/reference'
|
||||
import {
|
||||
createReferenceFieldsValidatorWithLabels,
|
||||
createReferenceValidatorWithLabels,
|
||||
} from '@/shared/validators/reference'
|
||||
import type { EditDataType, ReferenceDetailsType } from '@/shared/types'
|
||||
|
||||
const createReferenceDetails = (
|
||||
@@ -86,6 +89,24 @@ describe('validateReference display labels', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateReferenceFields', () => {
|
||||
it('runs validators for configured fields missing from editData', () => {
|
||||
const validateFields = createReferenceFieldsValidatorWithLabels()
|
||||
|
||||
const errors = validateFields({
|
||||
ref_type_id: 1,
|
||||
title_primary: 'Article title',
|
||||
} as EditDataType<ReferenceDetailsType>)
|
||||
|
||||
expect(errors).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ name: 'date_primary', error: 'This field is required' },
|
||||
{ name: 'ref_authors', error: 'This field is required' },
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateReference year fields', () => {
|
||||
it('rejects a future primary year', () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2026-01-01T00:00:00.000Z'))
|
||||
|
||||
Reference in New Issue
Block a user