Enforce integer ranges for numeric fields

This commit is contained in:
karilint
2026-04-20 14:04:27 +03:00
parent 82bcc1842e
commit 9e0780e03e
14 changed files with 213 additions and 44 deletions
@@ -129,9 +129,12 @@ export const ClimateTab = () => {
]
const pollenRecord = [
['Arboreal pollen (AP%)', textField('pers_pollen_ap', { type: 'number' })],
['Non-arboreal pollen (NAP%)', textField('pers_pollen_nap', { type: 'number' })],
['Other pollen (OP%)', textField('pers_pollen_other', { type: 'number' })],
['Arboreal pollen (AP%)', textField('pers_pollen_ap', { type: 'number', integerOnly: true, min: 0, max: 100 })],
[
'Non-arboreal pollen (NAP%)',
textField('pers_pollen_nap', { type: 'number', integerOnly: true, min: 0, max: 100 }),
],
['Other pollen (OP%)', textField('pers_pollen_other', { type: 'number', integerOnly: true, min: 0, max: 100 })],
]
const pollenTotalError = ['pers_pollen_ap', 'pers_pollen_nap', 'pers_pollen_other']
@@ -13,8 +13,11 @@ export const EcometricsTab = () => {
textField('estimate_precip', { type: 'number', integerOnly: true, min: 0 }),
],
['Estimate of mean annual temperature (°C)', textField('estimate_temp', { type: 'number' })],
['Estimate of net primary productivity (g/m2/yr)', textField('estimate_npp', { type: 'number' })],
['Woody cover percentage', textField('pers_woody_cover', { type: 'number' })],
[
'Estimate of net primary productivity (g/m2/yr)',
textField('estimate_npp', { type: 'number', integerOnly: true, min: 0 }),
],
['Woody cover percentage', textField('pers_woody_cover', { type: 'number', integerOnly: true, min: 0, max: 100 })],
]
return (
@@ -135,7 +135,7 @@ export const LocalityTab = () => {
'Approximate Coordinates'
),
],
['Altitude (m)', textField('altitude')],
['Altitude (m)', textField('altitude', { type: 'number', integerOnly: true, min: -500, max: 9000 })],
]
const { notify } = useNotify()
@@ -118,9 +118,9 @@ export const TaphonomyTab = () => {
const taphonomy = [
['Time Represented (years)', dropdown('time_rep', timeRepresentedOptions, 'Time Represented (years)')],
['Vertebrate Preservation', dropdown('vert_pres', vertebratePreservationOptions, 'Vertebrate Preservation')],
['Approx. Number of Specimens', textField('appr_num_spm', { type: 'number' })],
['Exact Number of Specimens', textField('num_spm', { type: 'number' })],
['Number of Quadrats', textField('num_quad', { type: 'number' })],
['Approx. Number of Specimens', textField('appr_num_spm', { type: 'number', integerOnly: true, min: 1 })],
['Exact Number of Specimens', textField('num_spm', { type: 'number', integerOnly: true, min: 1 })],
['Number of Quadrats', textField('num_quad', { type: 'number', integerOnly: true, min: 1 })],
['Unbiased Collecting', dropdown('true_quant', unbiasedCollectingOptions, 'Unbiased Collecting')],
['Species List Complete', dropdown('complete', speciesListCompleteOptions, 'Species List Complete')],
]
@@ -43,10 +43,19 @@ export const OccurrenceCoreTab = () => {
key="counts"
title="Occurrence counts"
array={[
['NISP', mode.read ? toText(sourceData.nis) : textField('nis', { type: 'number' })],
[
'NISP',
mode.read ? toText(sourceData.nis) : textField('nis', { type: 'number', integerOnly: true, min: 1 }),
],
['Percent', mode.read ? toText(sourceData.pct) : textField('pct', { type: 'number' })],
['Quadrate', mode.read ? toText(sourceData.quad) : textField('quad', { type: 'number' })],
['MNI', mode.read ? toText(sourceData.mni) : textField('mni', { type: 'number' })],
[
'Quadrate',
mode.read ? toText(sourceData.quad) : textField('quad', { type: 'number', integerOnly: true, min: 1 }),
],
[
'MNI',
mode.read ? toText(sourceData.mni) : textField('mni', { type: 'number', integerOnly: true, min: 1 }),
],
['Quantity', mode.read ? formatQuantity(sourceData.qua) : dropdown('qua', quantityOptions, 'Quantity')],
]}
/>,
@@ -54,7 +63,12 @@ export const OccurrenceCoreTab = () => {
</HalfFrames>
<ArrayFrame
array={[
['Body Mass (g)', mode.read ? toText(sourceData.body_mass) : textField('body_mass', { type: 'number' })],
[
'Body Mass (g)',
mode.read
? toText(sourceData.body_mass)
: textField('body_mass', { type: 'number', integerOnly: true, min: 1 }),
],
]}
title="Size"
/>
@@ -16,7 +16,12 @@ export const OccurrenceIsotopeTab = () => {
title="δ13C"
array={[
['Mean', mode.read ? toText(sourceData.dc13_mean) : textField('dc13_mean', { type: 'number' })],
['N', mode.read ? toText(sourceData.dc13_n) : textField('dc13_n', { type: 'number' })],
[
'N',
mode.read
? toText(sourceData.dc13_n)
: textField('dc13_n', { type: 'number', integerOnly: true, min: 1 }),
],
['Max', mode.read ? toText(sourceData.dc13_max) : textField('dc13_max', { type: 'number' })],
['Min', mode.read ? toText(sourceData.dc13_min) : textField('dc13_min', { type: 'number' })],
['Stdev', mode.read ? toText(sourceData.dc13_stdev) : textField('dc13_stdev', { type: 'number' })],
@@ -27,7 +32,12 @@ export const OccurrenceIsotopeTab = () => {
title="δ18O"
array={[
['Mean', mode.read ? toText(sourceData.do18_mean) : textField('do18_mean', { type: 'number' })],
['N', mode.read ? toText(sourceData.do18_n) : textField('do18_n', { type: 'number' })],
[
'N',
mode.read
? toText(sourceData.do18_n)
: textField('do18_n', { type: 'number', integerOnly: true, min: 1 }),
],
['Max', mode.read ? toText(sourceData.do18_max) : textField('do18_max', { type: 'number' })],
['Min', mode.read ? toText(sourceData.do18_min) : textField('do18_min', { type: 'number' })],
['Stdev', mode.read ? toText(sourceData.do18_stdev) : textField('do18_stdev', { type: 'number' })],
@@ -33,11 +33,36 @@ export const OccurrenceWearTab = () => {
'Mesowear',
mode.read ? formatMesowear(sourceData.mesowear) : dropdown('mesowear', mesowearOptions, 'Mesowear'),
],
['MW OR High', mode.read ? toText(sourceData.mw_or_high) : textField('mw_or_high', { type: 'number' })],
['MW OR Low', mode.read ? toText(sourceData.mw_or_low) : textField('mw_or_low', { type: 'number' })],
['MW CS Sharp', mode.read ? toText(sourceData.mw_cs_sharp) : textField('mw_cs_sharp', { type: 'number' })],
['MW CS Round', mode.read ? toText(sourceData.mw_cs_round) : textField('mw_cs_round', { type: 'number' })],
['MW CS Blunt', mode.read ? toText(sourceData.mw_cs_blunt) : textField('mw_cs_blunt', { type: 'number' })],
[
'MW OR High',
mode.read
? toText(sourceData.mw_or_high)
: textField('mw_or_high', { type: 'number', integerOnly: true, min: 0, max: 100 }),
],
[
'MW OR Low',
mode.read
? toText(sourceData.mw_or_low)
: textField('mw_or_low', { type: 'number', integerOnly: true, min: 0, max: 100 }),
],
[
'MW CS Sharp',
mode.read
? toText(sourceData.mw_cs_sharp)
: textField('mw_cs_sharp', { type: 'number', integerOnly: true, min: 0, max: 100 }),
],
[
'MW CS Round',
mode.read
? toText(sourceData.mw_cs_round)
: textField('mw_cs_round', { type: 'number', integerOnly: true, min: 0, max: 100 }),
],
[
'MW CS Blunt',
mode.read
? toText(sourceData.mw_cs_blunt)
: textField('mw_cs_blunt', { type: 'number', integerOnly: true, min: 0, max: 100 }),
],
]}
/>,
<div key="wear-score-and-microwear">
@@ -47,11 +72,15 @@ export const OccurrenceWearTab = () => {
array={[
[
'MW scale min',
mode.read ? toText(sourceData.mw_scale_min) : textField('mw_scale_min', { type: 'number' }),
mode.read
? toText(sourceData.mw_scale_min)
: textField('mw_scale_min', { type: 'number', integerOnly: true, min: 0 }),
],
[
'MW scale max',
mode.read ? toText(sourceData.mw_scale_max) : textField('mw_scale_max', { type: 'number' }),
mode.read
? toText(sourceData.mw_scale_max)
: textField('mw_scale_max', { type: 'number', integerOnly: true, min: 0 }),
],
['MW value', mode.read ? toText(sourceData.mw_value) : textField('mw_value', { type: 'number' })],
['Normalized Score', normalizedScoreText],
@@ -98,9 +98,13 @@ export const ReferenceTab = () => {
? bigTextField(field.field_name! as keyof ReferenceDetailsType)
: textField(field.field_name! as keyof ReferenceDetailsType)
if (field.field_name && numberFields.includes(field.field_name)) {
const isPageField = field.field_name === 'start_page' || field.field_name === 'end_page'
nonAuthorFieldsArray.push([
field.ref_field_name,
textField(field.field_name as keyof ReferenceDetailsType, { type: 'number' }),
textField(field.field_name as keyof ReferenceDetailsType, {
type: 'number',
...(isPageField ? { integerOnly: true, min: 1 } : {}),
}),
])
} else if (field.field_name == 'exact_date') {
nonAuthorFieldsArray.push([
@@ -26,8 +26,8 @@ export const SizeTab = () => {
]
const size = [
['Body Mass (g)', textField('body_mass', { type: 'number' })],
['Brain Mass (g)', textField('brain_mass', { type: 'number' })],
['Body Mass (g)', textField('body_mass', { type: 'number', integerOnly: true, min: 1 })],
['Brain Mass (g)', textField('brain_mass', { type: 'number', integerOnly: true, min: 1 })],
['Snout-Vent Length', dropdown('sv_length', snoutVentLengthOptions, 'Snout-Vent Length')],
]
@@ -135,13 +135,13 @@ export const TeethTab = () => {
const mesowear = [
['Mesowear'],
['Type', dropdown('mesowear', mesowearOptions, 'Type')],
['Cusp Relief Low (OR%)', textField('mw_or_low', { type: 'number' })],
['Cusp Relief High (OR%)', textField('mw_or_high', { type: 'number' })],
['Cusp Shape Sharp (CS%)', textField('mw_cs_sharp', { type: 'number' })],
['Cusp Shape Rounded (CS%)', textField('mw_cs_round', { type: 'number' })],
['Cusp Shape Blunt (CS%)', textField('mw_cs_blunt', { type: 'number' })],
['Scale Minimum', textField('mw_scale_min', { type: 'number' })],
['Scale Maximum', textField('mw_scale_max', { type: 'number' })],
['Cusp Relief Low (OR%)', textField('mw_or_low', { type: 'number', integerOnly: true, min: 0, max: 100 })],
['Cusp Relief High (OR%)', textField('mw_or_high', { type: 'number', integerOnly: true, min: 0, max: 100 })],
['Cusp Shape Sharp (CS%)', textField('mw_cs_sharp', { type: 'number', integerOnly: true, min: 0, max: 100 })],
['Cusp Shape Rounded (CS%)', textField('mw_cs_round', { type: 'number', integerOnly: true, min: 0, max: 100 })],
['Cusp Shape Blunt (CS%)', textField('mw_cs_blunt', { type: 'number', integerOnly: true, min: 0, max: 100 })],
['Scale Minimum', textField('mw_scale_min', { type: 'number', integerOnly: true, min: 0 })],
['Scale Maximum', textField('mw_scale_max', { type: 'number', integerOnly: true, min: 0 })],
['Reported Value', textField('mw_value', { type: 'number' })],
[
'Normalized score',
+36 -1
View File
@@ -15,7 +15,18 @@ const validatePollenPercentage = (name: string, num: number) => {
}
const validateNonNegativeInteger = (name: string, num: number) => {
if (!Number.isInteger(num) || num < 0) return `${name} must be a positive integer.`
if (!Number.isInteger(num) || num < 0) return `${name} must be a non-negative integer.`
return
}
const validateIntegerInRange = (name: string, num: number, min: number, max: number) => {
if (!Number.isInteger(num)) return `${name} must be an integer value`
if (num < min || num > max) return `${name} must be between ${min} and ${max}`
return
}
const validatePositiveInteger = (name: string, num: number) => {
if (!Number.isInteger(num) || num < 1) return `${name} must be a positive integer.`
return
}
@@ -202,6 +213,30 @@ export const validateLocality = (
name: 'Estimate of annual precipitation (mm)',
asNumber: (num: number) => validateNonNegativeInteger('Estimate of annual precipitation (mm)', num),
},
altitude: {
name: 'Altitude (m)',
asNumber: (num: number) => validateIntegerInRange('Altitude (m)', num, -500, 9000),
},
appr_num_spm: {
name: 'Approx. Number of Specimens',
asNumber: (num: number) => validatePositiveInteger('Approx. Number of Specimens', num),
},
num_spm: {
name: 'Exact Number of Specimens',
asNumber: (num: number) => validatePositiveInteger('Exact Number of Specimens', num),
},
num_quad: {
name: 'Number of Quadrats',
asNumber: (num: number) => validatePositiveInteger('Number of Quadrats', num),
},
estimate_npp: {
name: 'Estimate of net primary productivity (g/m2/yr)',
asNumber: (num: number) => validateNonNegativeInteger('Estimate of net primary productivity (g/m2/yr)', num),
},
pers_woody_cover: {
name: 'Woody cover percentage',
asNumber: (num: number) => validateIntegerInRange('Woody cover percentage', num, 0, 100),
},
}
return validator<EditDataType<LocalityDetailsType>>(validators, editData, fieldName)
+18 -7
View File
@@ -23,6 +23,17 @@ const validatePositiveInteger = (name: string, value: number) => {
return
}
const validateNonNegativeInteger = (name: string, value: number) => {
if (!Number.isInteger(value) || value < 0) return `${name} must be a non-negative integer.`
return
}
const validateIntegerInRange = (name: string, value: number, min: number, max: number) => {
if (!Number.isInteger(value)) return `${name} must be a whole number.`
if (value < min || value > max) return `${name} must be between ${min} and ${max}.`
return
}
const validatePositiveDecimal = (name: string, value: number) => {
if (!Number.isFinite(value) || value <= 0) return `${name} must be a positive decimal number.`
return
@@ -61,15 +72,15 @@ export const validateOccurrence = (editData: EditableOccurrenceData, fieldName:
body_mass: { name: 'Body mass', asNumber: value => validatePositiveInteger('Body mass', value) },
dc13_n: { name: 'δ13C N', asNumber: value => validatePositiveInteger('δ13C N', value) },
do18_n: { name: 'δ18O N', asNumber: value => validatePositiveInteger('δ18O N', value) },
mw_or_high: { name: 'MW OR High', asNumber: value => validatePositiveInteger('MW OR High', value) },
mw_or_low: { name: 'MW OR Low', asNumber: value => validatePositiveInteger('MW OR Low', value) },
mw_cs_sharp: { name: 'MW CS Sharp', asNumber: value => validatePositiveInteger('MW CS Sharp', value) },
mw_cs_round: { name: 'MW CS Round', asNumber: value => validatePositiveInteger('MW CS Round', value) },
mw_cs_blunt: { name: 'MW CS Blunt', asNumber: value => validatePositiveInteger('MW CS Blunt', value) },
mw_or_high: { name: 'MW OR High', asNumber: value => validateIntegerInRange('MW OR High', value, 0, 100) },
mw_or_low: { name: 'MW OR Low', asNumber: value => validateIntegerInRange('MW OR Low', value, 0, 100) },
mw_cs_sharp: { name: 'MW CS Sharp', asNumber: value => validateIntegerInRange('MW CS Sharp', value, 0, 100) },
mw_cs_round: { name: 'MW CS Round', asNumber: value => validateIntegerInRange('MW CS Round', value, 0, 100) },
mw_cs_blunt: { name: 'MW CS Blunt', asNumber: value => validateIntegerInRange('MW CS Blunt', value, 0, 100) },
mw_scale_min: {
name: 'Scale Minimum',
asNumber: value => {
const positivityError = validatePositiveInteger('Scale Minimum', value)
const positivityError = validateNonNegativeInteger('Scale Minimum', value)
if (positivityError) return positivityError
if (hasNumericValue(editData.mw_scale_max) && value > editData.mw_scale_max)
return 'Scale Minimum cannot be greater than Scale Maximum.'
@@ -79,7 +90,7 @@ export const validateOccurrence = (editData: EditableOccurrenceData, fieldName:
mw_scale_max: {
name: 'Scale Maximum',
asNumber: value => {
const positivityError = validatePositiveInteger('Scale Maximum', value)
const positivityError = validateNonNegativeInteger('Scale Maximum', value)
if (positivityError) return positivityError
if (hasNumericValue(editData.mw_scale_min) && value < editData.mw_scale_min)
return 'Scale Maximum cannot be less than Scale Minimum.'
+16 -2
View File
@@ -109,6 +109,13 @@ const yearCheck: (year: number) => ValidationError = (year: number) => {
return null
}
const positiveIntegerCheck: (name: string, num: number) => ValidationError = (name: string, num: number) => {
if (!Number.isFinite(num)) return `${name} must be a valid number`
if (!Number.isInteger(num)) return `${name} must be a whole number`
if (num < 1) return `${name} must be a positive integer`
return null
}
export type ReferenceFieldDisplayNames = Partial<Record<keyof EditDataType<ReferenceDetailsType>, string>>
export type ReferenceDisplayLabelMap = Partial<Record<number, ReferenceFieldDisplayNames>>
@@ -217,12 +224,19 @@ export const validateReference = (
start_page: {
name: 'start_page',
required: false,
asNumber: true,
asNumber: (num: number) => positiveIntegerCheck('start_page', num),
},
end_page: {
name: 'end_page',
required: false,
asNumber: true,
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',
+48 -2
View File
@@ -11,6 +11,22 @@ export const validateSpecies = (
editData: EditDataType<SpeciesDetailsType>,
fieldName: keyof EditDataType<SpeciesDetailsType>
) => {
const validatePositiveInteger = (name: string, value: number) => {
if (!Number.isInteger(value) || value < 1) return `${name} must be a positive integer.`
return
}
const validateNonNegativeInteger = (name: string, value: number) => {
if (!Number.isInteger(value) || value < 0) return `${name} must be a non-negative integer.`
return
}
const validateIntegerInRange = (name: string, value: number, min: number, max: number) => {
if (!Number.isInteger(value)) return `${name} must be a whole number.`
if (value < min || value > max) return `${name} must be between ${min} and ${max}.`
return
}
const validators: Validators<Partial<EditDataType<SpeciesDetailsType>>> = {
subclass_or_superorder_name: {
name: 'Subclass or Superorder',
@@ -103,11 +119,40 @@ export const validateSpecies = (
return
},
},
body_mass: {
name: 'Body Mass (g)',
asNumber: (value: number) => validatePositiveInteger('Body Mass (g)', value),
},
brain_mass: {
name: 'Brain Mass (g)',
asNumber: (value: number) => validatePositiveInteger('Brain Mass (g)', value),
},
mw_or_low: {
name: 'Cusp Relief Low (OR%)',
asNumber: (value: number) => validateIntegerInRange('Cusp Relief Low (OR%)', value, 0, 100),
},
mw_or_high: {
name: 'Cusp Relief High (OR%)',
asNumber: (value: number) => validateIntegerInRange('Cusp Relief High (OR%)', value, 0, 100),
},
mw_cs_sharp: {
name: 'Cusp Shape Sharp (CS%)',
asNumber: (value: number) => validateIntegerInRange('Cusp Shape Sharp (CS%)', value, 0, 100),
},
mw_cs_round: {
name: 'Cusp Shape Rounded (CS%)',
asNumber: (value: number) => validateIntegerInRange('Cusp Shape Rounded (CS%)', value, 0, 100),
},
mw_cs_blunt: {
name: 'Cusp Shape Blunt (CS%)',
asNumber: (value: number) => validateIntegerInRange('Cusp Shape Blunt (CS%)', value, 0, 100),
},
mw_scale_min: {
name: 'Scale Minimum',
asNumber: (value: number) => {
if (value < 0) return 'Scale Minimum cannot be negative.'
const nonNegativeError = validateNonNegativeInteger('Scale Minimum', value)
if (nonNegativeError) return nonNegativeError
if (typeof editData.mw_scale_max === 'number' && value > editData.mw_scale_max)
return 'Scale Minimum cannot be greater than Scale Maximum.'
return
@@ -116,7 +161,8 @@ export const validateSpecies = (
mw_scale_max: {
name: 'Scale Maximum',
asNumber: (value: number) => {
if (value < 0) return 'Scale Maximum cannot be negative.'
const nonNegativeError = validateNonNegativeInteger('Scale Maximum', value)
if (nonNegativeError) return nonNegativeError
if (typeof editData.mw_scale_min === 'number' && value < editData.mw_scale_min)
return 'Scale Maximum cannot be less than Scale Minimum.'
return