mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-18 03:13:47 +00:00
Merge branch '3408-fix--input-decimal-seperator' into 'develop'
Fix "number" validation for handling of decimal numbers with using comma or dot as decimal separator Closes #3408 See merge request baserow/baserow!3316
This commit is contained in:
commit
8c2c0f5d8b
16 changed files with 353 additions and 75 deletions
backend
src/baserow
tests/baserow
changelog/entries/unreleased/bug
web-frontend
|
@ -1,6 +1,7 @@
|
|||
import abc
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from decimal import InvalidOperation
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
|
@ -101,6 +102,7 @@ from baserow.core.formula.validator import (
|
|||
ensure_array,
|
||||
ensure_boolean,
|
||||
ensure_integer,
|
||||
ensure_numeric,
|
||||
ensure_string_or_integer,
|
||||
)
|
||||
from baserow.core.registry import Instance, T
|
||||
|
@ -1464,7 +1466,7 @@ class InputTextElementType(InputElementType):
|
|||
|
||||
def is_valid(
|
||||
self, element: InputTextElement, value: Any, dispatch_context: DispatchContext
|
||||
) -> bool:
|
||||
) -> Any:
|
||||
"""
|
||||
:param element: The element we're trying to use form data in.
|
||||
:param value: The form data value, which may be invalid.
|
||||
|
@ -1477,10 +1479,10 @@ class InputTextElementType(InputElementType):
|
|||
|
||||
elif element.validation_type == "integer":
|
||||
try:
|
||||
value = ensure_integer(value)
|
||||
except ValidationError as exc:
|
||||
return ensure_numeric(value)
|
||||
except (ValueError, TypeError, InvalidOperation, ValidationError) as exc:
|
||||
raise FormDataProviderChunkInvalidException(
|
||||
f"{value} must be a valid integer."
|
||||
f"{value} must be a valid number."
|
||||
) from exc
|
||||
|
||||
elif element.validation_type == "email":
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import json
|
||||
import re
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
from typing import Any, List, Optional, Union
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
|
@ -29,6 +31,50 @@ def ensure_boolean(value: Any) -> bool:
|
|||
raise ValidationError("Value is not a valid boolean or convertible to a boolean.")
|
||||
|
||||
|
||||
def ensure_numeric(
|
||||
value: Any, allow_null: bool = False
|
||||
) -> Optional[Union[int, float, Decimal]]:
|
||||
"""
|
||||
Ensures that the value is a number or can be converted to a numeric value.
|
||||
|
||||
:param value: The value to ensure as a number.
|
||||
:param allow_null: Whether to allow null or empty values.
|
||||
:return: The value as a number (int, float, or Decimal) if conversion is successful.
|
||||
:raises ValidationError: If the value is not a valid number or convertible
|
||||
to a number.
|
||||
"""
|
||||
|
||||
if allow_null and (value is None or value == ""):
|
||||
return None
|
||||
|
||||
# Handle numeric types directly
|
||||
if isinstance(value, (int, float, Decimal)) and not isinstance(value, bool):
|
||||
return value
|
||||
|
||||
# Handle string conversion
|
||||
if isinstance(value, str):
|
||||
# Check if the string matches a valid number pattern
|
||||
if re.match(r"^([-+])?(\d+(\.\d+)?)$", value):
|
||||
# Convert to int if it's a whole number, otherwise float
|
||||
try:
|
||||
num_value = float(value)
|
||||
if num_value.is_integer():
|
||||
return int(value)
|
||||
return num_value
|
||||
except ValueError:
|
||||
# If float conversion fails, try Decimal as a fallback
|
||||
try:
|
||||
return Decimal(value)
|
||||
except Exception as exc:
|
||||
raise ValidationError(
|
||||
f"Value '{value}' is not a valid number or convertible to a number."
|
||||
) from exc
|
||||
|
||||
raise ValidationError(
|
||||
f"Value '{value}' is not a valid number or convertible to a number."
|
||||
)
|
||||
|
||||
|
||||
def ensure_integer(value: Any, allow_empty: bool = False) -> Optional[int]:
|
||||
"""
|
||||
Ensures that the value is an integer or can be converted to an integer.
|
||||
|
|
|
@ -506,48 +506,43 @@ def test_choice_element_import_export_formula(data_fixture):
|
|||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_input_text_element_is_valid(data_fixture):
|
||||
validity_tests = [
|
||||
{"required": True, "type": "integer", "value": "", "result": False},
|
||||
{"required": True, "type": "integer", "value": 42, "result": 42},
|
||||
{"required": True, "type": "integer", "value": "42", "result": 42},
|
||||
{"required": True, "type": "integer", "value": "horse", "result": False},
|
||||
{"required": False, "type": "integer", "value": "", "result": ""},
|
||||
{
|
||||
"required": True,
|
||||
"type": "email",
|
||||
"value": "foo@bar.com",
|
||||
"result": "foo@bar.com",
|
||||
},
|
||||
{"required": True, "type": "email", "value": "foobar.com", "result": False},
|
||||
{"required": False, "type": "email", "value": "", "result": ""},
|
||||
{"required": True, "type": "any", "value": "", "result": False},
|
||||
{"required": True, "type": "any", "value": 42, "result": 42},
|
||||
{"required": True, "type": "any", "value": "42", "result": "42"},
|
||||
{"required": True, "type": "any", "value": "horse", "result": "horse"},
|
||||
{"required": False, "type": "any", "value": "", "result": ""},
|
||||
]
|
||||
for test in validity_tests:
|
||||
if test["result"] is not False:
|
||||
assert (
|
||||
InputTextElementType().is_valid(
|
||||
InputTextElement(
|
||||
validation_type=test["type"], required=test["required"]
|
||||
),
|
||||
test["value"],
|
||||
{},
|
||||
)
|
||||
== test["result"]
|
||||
), repr(test["value"])
|
||||
else:
|
||||
with pytest.raises(FormDataProviderChunkInvalidException):
|
||||
InputTextElementType().is_valid(
|
||||
InputTextElement(
|
||||
validation_type=test["type"], required=test["required"]
|
||||
),
|
||||
test["value"],
|
||||
{},
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"required,type,value,result",
|
||||
[
|
||||
(True, "integer", "", False),
|
||||
(True, "integer", 42, 42),
|
||||
(True, "integer", "4.2", 4.2),
|
||||
(True, "integer", "4,2", False),
|
||||
(True, "integer", "42", 42),
|
||||
(True, "integer", "horse", False),
|
||||
(False, "integer", "", ""),
|
||||
(True, "email", "foo@bar.com", "foo@bar.com"),
|
||||
(True, "email", "foobar.com", False),
|
||||
(False, "email", "", ""),
|
||||
(True, "any", "", False),
|
||||
(True, "any", 42, 42),
|
||||
(True, "any", "42", "42"),
|
||||
(True, "any", "horse", "horse"),
|
||||
(False, "any", "", ""),
|
||||
],
|
||||
)
|
||||
def test_input_text_element_is_valid(data_fixture, required, type, value, result):
|
||||
if result is not False:
|
||||
assert (
|
||||
InputTextElementType().is_valid(
|
||||
InputTextElement(validation_type=type, required=required),
|
||||
value,
|
||||
{},
|
||||
)
|
||||
== result
|
||||
), repr(f"{value} != {result}")
|
||||
else:
|
||||
with pytest.raises(FormDataProviderChunkInvalidException):
|
||||
InputTextElementType().is_valid(
|
||||
InputTextElement(validation_type=type, required=required),
|
||||
value,
|
||||
{},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
from decimal import Decimal
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
import pytest
|
||||
|
||||
from baserow.core.formula.validator import ensure_string, ensure_string_or_integer
|
||||
from baserow.core.formula.validator import (
|
||||
ensure_numeric,
|
||||
ensure_string,
|
||||
ensure_string_or_integer,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [0, 1, 10, 100])
|
||||
|
@ -191,3 +196,104 @@ def test_ensure_string_returns_str_by_default(value, expected):
|
|||
|
||||
result = ensure_string(value)
|
||||
assert result == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value,expected",
|
||||
[
|
||||
(0, 0),
|
||||
(1, 1),
|
||||
(10, 10),
|
||||
(-5, -5),
|
||||
(3.14, 3.14),
|
||||
(-2.5, -2.5),
|
||||
(Decimal("10.5"), Decimal("10.5")),
|
||||
("0", 0),
|
||||
("1", 1),
|
||||
("10", 10),
|
||||
("-5", -5),
|
||||
("3.14", 3.14),
|
||||
("-2.5", -2.5),
|
||||
("10.5", 10.5),
|
||||
],
|
||||
)
|
||||
def test_ensure_numeric_returns_correct_numeric_value(value, expected):
|
||||
"""
|
||||
Test the ensure_numeric() function.
|
||||
|
||||
Ensure that valid numeric values or
|
||||
convertible values return the correct numeric type.
|
||||
"""
|
||||
|
||||
result = ensure_numeric(value)
|
||||
assert result == expected
|
||||
assert type(result) is type(expected)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value",
|
||||
[
|
||||
"abc",
|
||||
"1a",
|
||||
"a1",
|
||||
"1.2.3",
|
||||
"1,000",
|
||||
True,
|
||||
False,
|
||||
[],
|
||||
{},
|
||||
object(),
|
||||
],
|
||||
)
|
||||
def test_ensure_numeric_raises_error_for_invalid_values(value):
|
||||
"""
|
||||
Test the ensure_numeric() function.
|
||||
|
||||
Ensure a ValidationError is raised
|
||||
if the value cannot be converted to a numeric type.
|
||||
"""
|
||||
|
||||
with pytest.raises(ValidationError) as e:
|
||||
ensure_numeric(value)
|
||||
|
||||
assert f"Value '{value}' is not a valid number or convertible to a number." in str(
|
||||
e.value
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value,allow_null,expected",
|
||||
[
|
||||
(None, True, None),
|
||||
("", True, None),
|
||||
(None, False, None), # This will raise an error
|
||||
("", False, None), # This will raise an error
|
||||
],
|
||||
)
|
||||
def test_ensure_numeric_handles_null_values(value, allow_null, expected):
|
||||
"""
|
||||
Test the ensure_numeric() function.
|
||||
|
||||
Ensure that None or empty string values are handled correctly
|
||||
based on allow_null parameter.
|
||||
"""
|
||||
|
||||
if not allow_null:
|
||||
with pytest.raises(ValidationError):
|
||||
ensure_numeric(value, allow_null=allow_null)
|
||||
else:
|
||||
result = ensure_numeric(value, allow_null=allow_null)
|
||||
assert result is expected
|
||||
|
||||
|
||||
def test_ensure_numeric_with_very_large_numbers():
|
||||
"""
|
||||
Test the ensure_numeric() function with very large numbers.
|
||||
|
||||
Ensure that very large numbers are handled correctly.
|
||||
"""
|
||||
|
||||
large_number = "9" * 100
|
||||
result = ensure_numeric(large_number)
|
||||
assert isinstance(result, (int, float, Decimal))
|
||||
assert str(result) == large_number
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"type": "bug",
|
||||
"message": "Fix \"number\" validation for handling of decimal numbers with using comma or dot as decimal separator",
|
||||
"domain": "builder",
|
||||
"issue_number": 3408,
|
||||
"bullet_points": [],
|
||||
"created_at": "2025-03-27"
|
||||
}
|
|
@ -4,11 +4,11 @@
|
|||
ref="textarea"
|
||||
class="ab-input"
|
||||
style="resize: none"
|
||||
:value="value"
|
||||
:value="fromValue(value)"
|
||||
:placeholder="placeholder"
|
||||
:rows="rows"
|
||||
@blur="$emit('blur', $event)"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
@input="$emit('input', toValue($event.target.value))"
|
||||
@focus="$emit('focus', $event)"
|
||||
@click="$emit('click', $event)"
|
||||
></textarea>
|
||||
|
@ -17,10 +17,10 @@
|
|||
ref="input"
|
||||
:type="type"
|
||||
class="ab-input"
|
||||
:value="value"
|
||||
:value="fromValue(value)"
|
||||
:placeholder="placeholder"
|
||||
@blur="$emit('blur', $event)"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
@input="$emit('input', toValue($event.target.value))"
|
||||
@focus="$emit('focus', $event)"
|
||||
@click="$emit('click', $event)"
|
||||
/>
|
||||
|
@ -38,7 +38,7 @@ export default {
|
|||
* @type {string} - The value of the input.
|
||||
*/
|
||||
value: {
|
||||
type: String,
|
||||
type: [String, Number],
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
|
@ -50,6 +50,22 @@ export default {
|
|||
required: false,
|
||||
default: null,
|
||||
},
|
||||
/**
|
||||
* @type {Function} - The function to process user input before storing it.
|
||||
*/
|
||||
toValue: {
|
||||
type: Function,
|
||||
required: false,
|
||||
default: (value) => value,
|
||||
},
|
||||
/**
|
||||
* @type {Function} - The function to process/convert the value to a string.
|
||||
*/
|
||||
fromValue: {
|
||||
type: Function,
|
||||
required: false,
|
||||
default: (value) => value,
|
||||
},
|
||||
/**
|
||||
* @type {boolean} - Whether the input is multiline.
|
||||
*/
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
:style="getStyleOverride('input')"
|
||||
>
|
||||
<ABInput
|
||||
v-model="inputValue"
|
||||
v-model="computedValue"
|
||||
:placeholder="resolvedPlaceholder"
|
||||
:multiline="element.is_multiline"
|
||||
:rows="element.rows"
|
||||
|
@ -19,7 +19,11 @@
|
|||
|
||||
<script>
|
||||
import formElement from '@baserow/modules/builder/mixins/formElement'
|
||||
import { ensureString } from '@baserow/modules/core/utils/validator'
|
||||
import {
|
||||
ensureNumeric,
|
||||
ensureString,
|
||||
} from '@baserow/modules/core/utils/validator'
|
||||
import { parseLocalizedNumber } from '@baserow/modules/core/utils/string'
|
||||
|
||||
export default {
|
||||
name: 'InputTextElement',
|
||||
|
@ -38,9 +42,37 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
internalValue: '',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
computedValue: {
|
||||
get() {
|
||||
return this.internalValue
|
||||
},
|
||||
set(newValue) {
|
||||
this.inputValue = this.fromInternalValue(newValue)
|
||||
this.internalValue = newValue
|
||||
},
|
||||
},
|
||||
localeLanguage() {
|
||||
return this.$i18n.locale
|
||||
},
|
||||
resolvedDefaultValue() {
|
||||
return ensureString(this.resolveFormula(this.element.default_value))
|
||||
try {
|
||||
const value = this.resolveFormula(this.element.default_value)
|
||||
|
||||
return this.isNumericField
|
||||
? ensureNumeric(value, { allowNull: true })
|
||||
: ensureString(value)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
},
|
||||
isNumericField() {
|
||||
return this.element.validation_type === 'integer'
|
||||
},
|
||||
resolvedLabel() {
|
||||
return ensureString(this.resolveFormula(this.element.label))
|
||||
|
@ -53,11 +85,46 @@ export default {
|
|||
resolvedDefaultValue: {
|
||||
handler(value) {
|
||||
this.inputValue = value
|
||||
this.internalValue = this.toInternalValue(value)
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toInternalValue(value) {
|
||||
if (this.isNumericField) {
|
||||
if (value) {
|
||||
return new Intl.NumberFormat(this.localeLanguage, {
|
||||
useGrouping: false,
|
||||
}).format(value)
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
return ensureString(value)
|
||||
},
|
||||
|
||||
fromInternalValue(value) {
|
||||
if (this.isNumericField) {
|
||||
if (value) {
|
||||
try {
|
||||
return ensureNumeric(
|
||||
parseLocalizedNumber(value, this.localeLanguage),
|
||||
{
|
||||
allowNull: true,
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return value
|
||||
},
|
||||
|
||||
getErrorMessage() {
|
||||
switch (this.element.validation_type) {
|
||||
case 'integer':
|
||||
|
|
|
@ -14,6 +14,7 @@ import TableElementForm from '@baserow/modules/builder/components/elements/compo
|
|||
import {
|
||||
ensureArray,
|
||||
ensureBoolean,
|
||||
ensureNumeric,
|
||||
ensureInteger,
|
||||
ensurePositiveInteger,
|
||||
ensureString,
|
||||
|
@ -1140,7 +1141,7 @@ export class InputTextElementType extends FormElementType {
|
|||
}
|
||||
|
||||
formDataType(element) {
|
||||
return 'string'
|
||||
return element.validation_type === 'integer' ? 'number' : 'string'
|
||||
}
|
||||
|
||||
getDisplayName(element, applicationContext) {
|
||||
|
@ -1158,12 +1159,16 @@ export class InputTextElementType extends FormElementType {
|
|||
|
||||
getInitialFormDataValue(element, applicationContext) {
|
||||
try {
|
||||
return this.resolveFormula(element.default_value, {
|
||||
const value = this.resolveFormula(element.default_value, {
|
||||
element,
|
||||
...applicationContext,
|
||||
})
|
||||
|
||||
return element.validation_type === 'integer'
|
||||
? ensureNumeric(value, { allowNull: true })
|
||||
: ensureString(value)
|
||||
} catch {
|
||||
return ''
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -266,7 +266,7 @@
|
|||
"validationTypeAnyLabel": "Any",
|
||||
"validationTypeAnyDescription": "Allow any value to be set in this input.",
|
||||
"validationTypeIntegerLabel": "Number",
|
||||
"validationTypeIntegerDescription": "Enforce a number value in this input.",
|
||||
"validationTypeIntegerDescription": "Enforce a numeric value in this input (accepts integers and decimals).",
|
||||
"validationTypeEmailLabel": "Email",
|
||||
"validationTypeEmailDescription": "Enforce an email address value in this input.",
|
||||
"inputType": "Input type",
|
||||
|
|
|
@ -137,10 +137,29 @@ export const escapeRegExp = (string) => {
|
|||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
export const isNumeric = (value) => {
|
||||
export const isInteger = (value) => {
|
||||
return /^-?\d+$/.test(value)
|
||||
}
|
||||
|
||||
export const isNumeric = (value) => {
|
||||
return /^-?\d+([.]\d+)?$/.test(value)
|
||||
}
|
||||
|
||||
export const parseLocalizedNumber = (str, locale) => {
|
||||
const parts = new Intl.NumberFormat(locale).formatToParts(12345.6)
|
||||
let group = parts.find((p) => p.type === 'group')?.value || ''
|
||||
let decimal = parts.find((p) => p.type === 'decimal')?.value || '.'
|
||||
|
||||
// Escape special characters for regex
|
||||
group = group.replace(/[\u202F\u00A0\s]/g, '\\s') // match all common spaces
|
||||
decimal = decimal === '.' ? '\\.' : decimal
|
||||
group = group === '.' ? '\\.' : group
|
||||
|
||||
const groupRegex = new RegExp(group, 'g')
|
||||
// Remove group separator and replace decimal with "."
|
||||
return str.replace(groupRegex, '').replace(decimal, '.')
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow to find the next unused name excluding a list of names.
|
||||
* This is the frontend equivalent of backend ".find_unused_name()" method.
|
||||
|
|
|
@ -12,7 +12,7 @@ import moment from '@baserow/modules/core/moment'
|
|||
* @throws {Error} If the value is not a valid number or convertible to an number.
|
||||
*/
|
||||
export const ensureNumeric = (value, { allowNull = false } = {}) => {
|
||||
if (allowNull && (value === null || value === '')) {
|
||||
if (allowNull && (value === null || value === '' || value === undefined)) {
|
||||
return null
|
||||
}
|
||||
if (Number.isFinite(value)) {
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { isNumeric } from '@baserow/modules/core/utils/string'
|
||||
import { isInteger } from '@baserow/modules/core/utils/string'
|
||||
import { getPersistentFieldOptionsKey } from '@baserow/modules/database/utils/field'
|
||||
import PaginatedDropdown from '@baserow/modules/core/components/PaginatedDropdown'
|
||||
import SelectRowModal from '@baserow/modules/database/components/row/SelectRowModal'
|
||||
|
@ -61,7 +61,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
valid() {
|
||||
return isNumeric(this.filter.value)
|
||||
return isInteger(this.filter.value)
|
||||
},
|
||||
isDropdown() {
|
||||
return this.readOnly && this.view && this.isPublicView
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
import {
|
||||
collatedStringCompare,
|
||||
getFilenameFromUrl,
|
||||
isNumeric,
|
||||
isInteger,
|
||||
isSimplePhoneNumber,
|
||||
isValidEmail,
|
||||
isValidURL,
|
||||
|
@ -2842,7 +2842,7 @@ export class DurationFieldType extends FieldType {
|
|||
}
|
||||
|
||||
prepareValueForPaste(field, clipboardData, richClipboardData) {
|
||||
if (richClipboardData && isNumeric(richClipboardData)) {
|
||||
if (richClipboardData && isInteger(richClipboardData)) {
|
||||
return richClipboardData
|
||||
}
|
||||
return this.parseInputValue(field, clipboardData)
|
||||
|
@ -3383,7 +3383,7 @@ export class SingleSelectFieldType extends SelectOptionBaseFieldType {
|
|||
}
|
||||
|
||||
_findOptionWithMatchingId(field, rawTextValue) {
|
||||
if (isNumeric(rawTextValue)) {
|
||||
if (isInteger(rawTextValue)) {
|
||||
const pastedOptionId = parseInt(rawTextValue, 10)
|
||||
return field.select_options.find((option) => option.id === pastedOptionId)
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ import {
|
|||
DATE_FILTER_OPERATOR_BOUNDS,
|
||||
DateFilterOperators,
|
||||
} from '@baserow/modules/database/utils/date'
|
||||
import { isNumeric } from '@baserow/modules/core/utils/string'
|
||||
import { isInteger } from '@baserow/modules/core/utils/string'
|
||||
import ViewFilterTypeFileTypeDropdown from '@baserow/modules/database/components/view/ViewFilterTypeFileTypeDropdown'
|
||||
import ViewFilterTypeCollaborators from '@baserow/modules/database/components/view/ViewFilterTypeCollaborators'
|
||||
import {
|
||||
|
@ -2446,7 +2446,7 @@ export class MultipleCollaboratorsHasFilterType extends ViewFilterType {
|
|||
}
|
||||
|
||||
matches(rowValue, filterValue, field, fieldType) {
|
||||
if (!isNumeric(filterValue)) {
|
||||
if (!isInteger(filterValue)) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -2481,7 +2481,7 @@ export class MultipleCollaboratorsHasNotFilterType extends ViewFilterType {
|
|||
}
|
||||
|
||||
matches(rowValue, filterValue, field, fieldType) {
|
||||
if (!isNumeric(filterValue)) {
|
||||
if (!isInteger(filterValue)) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -2517,7 +2517,7 @@ export class UserIsFilterType extends ViewFilterType {
|
|||
}
|
||||
|
||||
matches(rowValue, filterValue, field, fieldType) {
|
||||
if (!isNumeric(filterValue)) {
|
||||
if (!isInteger(filterValue)) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -2553,7 +2553,7 @@ export class UserIsNotFilterType extends ViewFilterType {
|
|||
}
|
||||
|
||||
matches(rowValue, filterValue, field, fieldType) {
|
||||
if (!isNumeric(filterValue)) {
|
||||
if (!isInteger(filterValue)) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -2624,7 +2624,7 @@ export class LinkRowHasFilterType extends ViewFilterType {
|
|||
}
|
||||
|
||||
matches(rowValue, filterValue, field, fieldType) {
|
||||
if (!isNumeric(filterValue)) {
|
||||
if (!isInteger(filterValue)) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -2656,7 +2656,7 @@ export class LinkRowHasNotFilterType extends ViewFilterType {
|
|||
}
|
||||
|
||||
matches(rowValue, filterValue, field, fieldType) {
|
||||
if (!isNumeric(filterValue)) {
|
||||
if (!isInteger(filterValue)) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
isValidEmail,
|
||||
isSecureURL,
|
||||
isNumeric,
|
||||
isInteger,
|
||||
isSubstringOfStrings,
|
||||
} from '@baserow/modules/core/utils/string'
|
||||
|
||||
|
@ -131,7 +132,8 @@ describe('test string utils', () => {
|
|||
|
||||
test('test isNumeric', () => {
|
||||
expect(isNumeric('a')).toBe(false)
|
||||
expect(isNumeric('1.2')).toBe(false)
|
||||
expect(isNumeric('1.2')).toBe(true)
|
||||
expect(isNumeric('1,2')).toBe(false)
|
||||
expect(isNumeric('')).toBe(false)
|
||||
expect(isNumeric('null')).toBe(false)
|
||||
expect(isNumeric('12px')).toBe(false)
|
||||
|
@ -140,6 +142,17 @@ describe('test string utils', () => {
|
|||
expect(isNumeric('-100')).toBe(true)
|
||||
})
|
||||
|
||||
test('test isInteger', () => {
|
||||
expect(isInteger('a')).toBe(false)
|
||||
expect(isInteger('1.2')).toBe(false)
|
||||
expect(isInteger('1,2')).toBe(false)
|
||||
expect(isInteger('')).toBe(false)
|
||||
expect(isInteger('null')).toBe(false)
|
||||
expect(isInteger('12px')).toBe(false)
|
||||
expect(isInteger('1')).toBe(true)
|
||||
expect(isInteger('9999')).toBe(true)
|
||||
expect(isInteger('-100')).toBe(true)
|
||||
})
|
||||
test('test isSubstringOfStrings', () => {
|
||||
expect(isSubstringOfStrings(['hello'], 'hell')).toBe(true)
|
||||
expect(isSubstringOfStrings(['test'], 'hell')).toBe(false)
|
||||
|
|
|
@ -111,7 +111,9 @@ describe('ensureNumeric', () => {
|
|||
// Test null handling
|
||||
test('handles null values based on allowNull option', () => {
|
||||
expect(() => ensureNumeric(null)).toThrow()
|
||||
expect(() => ensureNumeric(undefined)).toThrow()
|
||||
expect(ensureNumeric(null, { allowNull: true })).toBeNull()
|
||||
expect(ensureNumeric(undefined, { allowNull: true })).toBeNull()
|
||||
})
|
||||
|
||||
// Test empty string handling
|
||||
|
@ -143,7 +145,6 @@ describe('ensureNumeric', () => {
|
|||
|
||||
test('throws error for undefined', () => {
|
||||
expect(() => ensureNumeric(undefined)).toThrow()
|
||||
expect(() => ensureNumeric(undefined, { allowNull: true })).toThrow()
|
||||
})
|
||||
|
||||
// Test error message
|
||||
|
|
Loading…
Add table
Reference in a new issue