mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-03 04:35:31 +00:00
Add array support to query parameters validation handlers
This commit is contained in:
parent
5fc1ef2d1b
commit
332ec39ef6
14 changed files with 461 additions and 129 deletions
backend
src/baserow/contrib/integrations/local_baserow
tests/baserow/contrib/integrations/local_baserow/service_types
changelog/entries/unreleased/feature
e2e-tests/tests/builder
web-frontend
modules
test/unit/core/utils
|
@ -172,7 +172,7 @@ class LocalBaserowTableServiceFilterableMixin:
|
|||
|
||||
if service_filter.value_is_formula:
|
||||
try:
|
||||
resolved_value = str(
|
||||
resolved_value = ensure_string(
|
||||
resolve_formula(
|
||||
service_filter.value,
|
||||
formula_runtime_function_registry,
|
||||
|
|
|
@ -11,6 +11,7 @@ from baserow.contrib.database.api.rows.serializers import RowSerializer
|
|||
from baserow.contrib.database.rows.handler import RowHandler
|
||||
from baserow.contrib.database.table.handler import TableHandler
|
||||
from baserow.contrib.database.views.models import SORT_ORDER_ASC, SORT_ORDER_DESC
|
||||
from baserow.contrib.database.views.view_filters import MultipleSelectHasViewFilterType
|
||||
from baserow.contrib.integrations.local_baserow.models import LocalBaserowListRows
|
||||
from baserow.contrib.integrations.local_baserow.service_types import (
|
||||
LocalBaserowListRowsUserServiceType,
|
||||
|
@ -1162,3 +1163,63 @@ def test_extract_properties(path, expected):
|
|||
result = service_type.extract_properties(path)
|
||||
|
||||
assert result == expected
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_search_on_multiple_select_with_list(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
workspace = data_fixture.create_workspace(user=user)
|
||||
database = data_fixture.create_database_application(workspace=workspace)
|
||||
page = data_fixture.create_builder_page(
|
||||
user=user, path="/page/", query_params=[{"name": "foobar", "type": "numeric"}]
|
||||
)
|
||||
|
||||
service_type = service_type_registry.get("local_baserow_list_rows")
|
||||
integration = data_fixture.create_local_baserow_integration(
|
||||
application=page.builder, user=user
|
||||
)
|
||||
table = data_fixture.create_database_table(database=database)
|
||||
view = data_fixture.create_grid_view(user, table=table)
|
||||
option_field = data_fixture.create_multiple_select_field(
|
||||
table=table, name="option_field", order=1, primary=True
|
||||
)
|
||||
options = [
|
||||
data_fixture.create_select_option(
|
||||
field=option_field, value="A", color="AAA", order=0
|
||||
),
|
||||
data_fixture.create_select_option(
|
||||
field=option_field, value="B", color="BBB", order=0
|
||||
),
|
||||
data_fixture.create_select_option(
|
||||
field=option_field, value="C", color="CCC", order=0
|
||||
),
|
||||
]
|
||||
table_rows = data_fixture.create_rows_in_table(
|
||||
table=table,
|
||||
rows=[[(options[0].id,)], [(options[1].id,)], [(options[1].id,)]],
|
||||
fields=[option_field],
|
||||
)
|
||||
|
||||
service = data_fixture.create_local_baserow_list_rows_service(
|
||||
integration=integration,
|
||||
view=view,
|
||||
table=view.table,
|
||||
search_query="",
|
||||
filter_type="OR",
|
||||
)
|
||||
|
||||
data_fixture.create_local_baserow_table_service_filter(
|
||||
service=service,
|
||||
field=option_field,
|
||||
value="get('foobar')",
|
||||
order=0,
|
||||
type=MultipleSelectHasViewFilterType.type,
|
||||
value_is_formula=True,
|
||||
)
|
||||
|
||||
dispatch_context = FakeDispatchContext(context={"foobar": [options[0].id]})
|
||||
dispatch_data = service_type.dispatch_data(
|
||||
service, {"table": table}, dispatch_context
|
||||
)
|
||||
assert len(dispatch_data["results"]) == 1
|
||||
assert dispatch_data["results"][0]._primary_field_id == options[0].field_id
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"type": "feature",
|
||||
"message": "Add support for list type values for query parameters.",
|
||||
"domain": "builder",
|
||||
"issue_number": 3433,
|
||||
"bullet_points": [],
|
||||
"created_at": "2025-02-20"
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
import {expect, test} from "../baserowTest";
|
||||
import { expect, test } from "../baserowTest";
|
||||
|
||||
test.describe("Builder page test suite", () => {
|
||||
test.beforeEach(async ({builderPagePage}) => {
|
||||
test.beforeEach(async ({ builderPagePage }) => {
|
||||
await builderPagePage.goto();
|
||||
});
|
||||
|
||||
test("Can create a page", async ({page}) => {
|
||||
test("Can create a page", async ({ page }) => {
|
||||
await page.getByText("New page").click();
|
||||
await page.getByText("Create page").waitFor();
|
||||
await page
|
||||
|
@ -25,12 +25,12 @@ test.describe("Builder page test suite", () => {
|
|||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("Can open page settings", async ({page}) => {
|
||||
test("Can open page settings", async ({ page }) => {
|
||||
await page.getByText("Page settings").click();
|
||||
await expect(page.locator(".box__title").getByText("Page")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Can change page settings", async ({page}) => {
|
||||
test("Can change page settings", async ({ page }) => {
|
||||
await page.getByText("Page settings").click();
|
||||
|
||||
await page
|
||||
|
@ -57,9 +57,9 @@ test.describe("Builder page test suite", () => {
|
|||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("Can create an element from empty page", async ({page}) => {
|
||||
test("Can create an element from empty page", async ({ page }) => {
|
||||
await page.getByText("Click to create an element").click();
|
||||
await page.getByText("Heading", {exact: true}).click();
|
||||
await page.getByText("Heading", { exact: true }).click();
|
||||
|
||||
await expect(
|
||||
page.locator(".modal__box").getByText("Add new element")
|
||||
|
@ -69,13 +69,13 @@ test.describe("Builder page test suite", () => {
|
|||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("Can create an element from element menu", async ({page}) => {
|
||||
test("Can create an element from element menu", async ({ page }) => {
|
||||
await page.locator(".header").getByText("Elements").click();
|
||||
await page
|
||||
.locator(".elements-context")
|
||||
.getByText("Element", {exact: true})
|
||||
.getByText("Element", { exact: true })
|
||||
.click();
|
||||
await page.getByText("Heading", {exact: true}).click();
|
||||
await page.getByText("Heading", { exact: true }).click();
|
||||
|
||||
await expect(
|
||||
page.locator(".modal__box").getByText("Add new element")
|
||||
|
@ -85,8 +85,7 @@ test.describe("Builder page test suite", () => {
|
|||
).toBeVisible();
|
||||
});
|
||||
|
||||
|
||||
test("Can add query parameter to page setting", async ({page}) => {
|
||||
test("Can add query parameter to page setting", async ({ page }) => {
|
||||
await page.getByText("Page settings").click();
|
||||
|
||||
await page
|
||||
|
@ -94,13 +93,33 @@ test.describe("Builder page test suite", () => {
|
|||
.getByPlaceholder("Enter a name...")
|
||||
.fill("New page name");
|
||||
|
||||
await page.getByRole('button', {name: 'Add query string parameter'}).click();
|
||||
await page
|
||||
.getByRole("button", { name: "Add query string parameter" })
|
||||
.click();
|
||||
await page
|
||||
.getByRole("button", { name: "Add another query string parameter" })
|
||||
.click();
|
||||
|
||||
await page
|
||||
.locator(".page-settings-query-params .form-input__wrapper")
|
||||
.getByRole('textbox')
|
||||
.getByRole("textbox")
|
||||
.nth(1)
|
||||
.fill("my_param");
|
||||
|
||||
await page
|
||||
.locator(".page-settings-query-params .form-input__wrapper")
|
||||
.getByRole("textbox")
|
||||
.nth(0)
|
||||
.fill("my_param2");
|
||||
|
||||
await page.locator("a").filter({ hasText: "Text" }).first().click();
|
||||
await page
|
||||
.locator("form")
|
||||
.getByRole("list")
|
||||
.locator("a")
|
||||
.filter({ hasText: "Numeric" })
|
||||
.click();
|
||||
|
||||
await page.locator(".button").getByText("Save").click();
|
||||
await expect(
|
||||
page.getByText("The page settings have been updated.")
|
||||
|
@ -108,20 +127,33 @@ test.describe("Builder page test suite", () => {
|
|||
|
||||
await page.getByTitle("Close").click();
|
||||
await expect(page.locator(".box__title").getByText("Page")).toBeHidden();
|
||||
await page.getByText('Click to create an element').click();
|
||||
await page.locator('.add-element-card__element-type-icon-link').click();
|
||||
await page.getByLabel('my_param=').fill("foo")
|
||||
await page.getByRole('complementary').getByRole('textbox').click();
|
||||
await page.getByRole('complementary').getByRole('textbox').locator('div').first().fill('linkim');
|
||||
await page.locator('a').filter({hasText: 'Make a choice'}).click();
|
||||
await page.locator('a').filter({hasText: '?my_param=*'}).click();
|
||||
await page.getByText("Click to create an element").click();
|
||||
await page.locator(".add-element-card__element-type-icon-link").click();
|
||||
await page.getByLabel("my_param=").fill("foo");
|
||||
await page.getByLabel("my_param2=").fill("15");
|
||||
await page.getByRole("complementary").getByRole("textbox").click();
|
||||
await page
|
||||
.getByRole("complementary")
|
||||
.getByRole("textbox")
|
||||
.locator("div")
|
||||
.first()
|
||||
.fill("linkim");
|
||||
await page.locator("a").filter({ hasText: "Make a choice" }).click();
|
||||
await page
|
||||
.locator("a")
|
||||
.filter({ hasText: "New page name /default/page?" })
|
||||
.click();
|
||||
// click empty place to close tooltip from prev. step
|
||||
await page.click('body')
|
||||
await page.getByRole('textbox').nth(2).click();
|
||||
await page.getByText('my_param', { exact: true }).first().click();
|
||||
await page.click('body')
|
||||
await expect(page.getByRole('link', {name: 'linkim'})).toHaveAttribute(
|
||||
'href', /\?my_param=foo/);
|
||||
|
||||
await page.click("body");
|
||||
await page.getByRole("textbox").nth(4).click();
|
||||
await page.getByText("my_param", { exact: true }).first().click();
|
||||
await page.locator('[id="__layout"]').getByRole("textbox").nth(3).click();
|
||||
await page.getByText("my_param2", { exact: true }).first().click();
|
||||
await page.click("body");
|
||||
await expect(page.getByRole("link", { name: "linkim" })).toHaveAttribute(
|
||||
"href",
|
||||
/\?my_param2=15&my_param=foo/
|
||||
);
|
||||
debugger;
|
||||
});
|
||||
});
|
||||
|
|
|
@ -61,6 +61,7 @@ import {
|
|||
ensureString,
|
||||
ensureStringOrInteger,
|
||||
ensureArray,
|
||||
ensurePositiveInteger,
|
||||
} from '@baserow/modules/core/utils/validator'
|
||||
import { CHOICE_OPTION_TYPES } from '@baserow/modules/builder/enums'
|
||||
|
||||
|
@ -94,20 +95,36 @@ export default {
|
|||
return ensureString(this.resolveFormula(this.element.placeholder))
|
||||
},
|
||||
defaultValueResolved() {
|
||||
let converter = ensureString
|
||||
if (
|
||||
this.optionsResolved.find(
|
||||
({ value }) => value !== undefined && value !== null // We skip null values
|
||||
) &&
|
||||
Number.isInteger(this.optionsResolved[0].value)
|
||||
) {
|
||||
converter = (v) => ensurePositiveInteger(v, { allowNull: true })
|
||||
}
|
||||
if (this.element.multiple) {
|
||||
const existingValues = this.optionsResolved.map(({ value }) => value)
|
||||
return ensureArray(this.resolveFormula(this.element.default_value))
|
||||
.map(ensureStringOrInteger)
|
||||
.filter((value) => existingValues.includes(value))
|
||||
try {
|
||||
const existingValues = this.optionsResolved.map(({ value }) => value)
|
||||
return ensureArray(this.resolveFormula(this.element.default_value))
|
||||
.map(converter)
|
||||
.filter((value) => existingValues.includes(value))
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
} else {
|
||||
// Always return a string if we have a default value, otherwise
|
||||
// set the value to null as single select fields will only skip
|
||||
// field preparation if the value is null.
|
||||
const resolvedSingleValue = ensureStringOrInteger(
|
||||
this.resolveFormula(this.element.default_value)
|
||||
)
|
||||
|
||||
return resolvedSingleValue === '' ? null : resolvedSingleValue
|
||||
try {
|
||||
// Always return a string if we have a default value, otherwise
|
||||
// set the value to null as single select fields will only skip
|
||||
// field preparation if the value is null.
|
||||
const resolvedSingleValue = converter(
|
||||
this.resolveFormula(this.element.default_value)
|
||||
)
|
||||
return resolvedSingleValue === '' ? null : resolvedSingleValue
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
},
|
||||
canHaveOptions() {
|
||||
|
|
|
@ -35,21 +35,22 @@
|
|||
class="preview-navigation-bar__query-separator"
|
||||
>
|
||||
{{ index === 0 ? '?' : '&' }}
|
||||
</span>
|
||||
<PreviewNavigationBarQueryParam
|
||||
:key="`param-${queryParam.key}`"
|
||||
:class="`preview-navigation-bar__query-parameter-input--${queryParam.type}`"
|
||||
:validation-fn="queryParam.validationFn"
|
||||
:default-value="pageParameters[queryParam.name]"
|
||||
:name="queryParam.name"
|
||||
@change="
|
||||
actionSetParameter({
|
||||
page,
|
||||
name: queryParam.name,
|
||||
value: $event,
|
||||
})
|
||||
"
|
||||
/>
|
||||
|
||||
<label :for="queryParam.name">{{ queryParam.name }}=</label>
|
||||
<PreviewNavigationBarInput
|
||||
:id="queryParam.name"
|
||||
:key="`param-${queryParam.key}`"
|
||||
:class="`preview-navigation-bar__query-parameter-input--${queryParam.type}`"
|
||||
:validation-fn="queryParam.validationFn"
|
||||
:default-value="pageParameters[queryParam.name]"
|
||||
@change="
|
||||
actionSetParameter({
|
||||
page,
|
||||
name: queryParam.name,
|
||||
value: $event,
|
||||
})
|
||||
"
|
||||
/></span>
|
||||
</template>
|
||||
</div>
|
||||
<div />
|
||||
|
@ -61,15 +62,13 @@ import { splitPath } from '@baserow/modules/builder/utils/path'
|
|||
import PreviewNavigationBarInput from '@baserow/modules/builder/components/page/PreviewNavigationBarInput'
|
||||
import UserSelector from '@baserow/modules/builder/components/page/UserSelector'
|
||||
import { mapActions } from 'vuex'
|
||||
import { PAGE_PARAM_TYPE_VALIDATION_FUNCTIONS } from '@baserow/modules/builder/enums'
|
||||
import PreviewNavigationBarQueryParam from '@baserow/modules/builder/components/page/PreviewNavigationBarQueryParam.vue'
|
||||
import {
|
||||
PAGE_PARAM_TYPE_VALIDATION_FUNCTIONS,
|
||||
QUERY_PARAM_TYPE_HANDLER_FUNCTIONS,
|
||||
} from '@baserow/modules/builder/enums'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PreviewNavigationBarInput,
|
||||
UserSelector,
|
||||
PreviewNavigationBarQueryParam,
|
||||
},
|
||||
components: { PreviewNavigationBarInput, UserSelector },
|
||||
props: {
|
||||
page: {
|
||||
type: Object,
|
||||
|
@ -84,7 +83,7 @@ export default {
|
|||
return this.page.query_params.map((queryParam, idx) => ({
|
||||
...queryParam,
|
||||
key: `query-param-${queryParam.name}-${idx}`,
|
||||
validationFn: PAGE_PARAM_TYPE_VALIDATION_FUNCTIONS[queryParam.type],
|
||||
validationFn: QUERY_PARAM_TYPE_HANDLER_FUNCTIONS[queryParam.type],
|
||||
}))
|
||||
},
|
||||
splitPath() {
|
||||
|
|
|
@ -9,10 +9,12 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
defaultValue: {
|
||||
type: [String, Number],
|
||||
type: [String, Number, Array],
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
|
@ -45,7 +47,9 @@ export default {
|
|||
},
|
||||
watch: {
|
||||
defaultValue(newValue) {
|
||||
this.inputValue = newValue
|
||||
if (!_.isEqual(this.inputValue, newValue)) {
|
||||
this.inputValue = newValue
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
<template>
|
||||
<div class="preview-navigation-bar-param">
|
||||
<label :for="name" class="preview-navigation-bar-param__label">
|
||||
{{ name }}=</label
|
||||
><input
|
||||
:id="name"
|
||||
v-model="inputValue"
|
||||
class="preview-navigation-bar-input"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
defaultValue: {
|
||||
type: [String, Number],
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
value: this.defaultValue,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
inputValue: {
|
||||
get() {
|
||||
return this.value
|
||||
},
|
||||
set(inputValue) {
|
||||
this.value = inputValue
|
||||
this.$emit('change', this.value)
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
defaultValue(newValue) {
|
||||
this.value = newValue
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -6,7 +6,7 @@ import { defaultValueForParameterType } from '@baserow/modules/builder/utils/par
|
|||
import { DEFAULT_USER_ROLE_PREFIX } from '@baserow/modules/builder/constants'
|
||||
import {
|
||||
PAGE_PARAM_TYPE_VALIDATION_FUNCTIONS,
|
||||
QUERY_PARAM_TYPE_VALIDATION_FUNCTIONS,
|
||||
QUERY_PARAM_TYPE_HANDLER_FUNCTIONS,
|
||||
} from '@baserow/modules/builder/enums'
|
||||
import { extractSubSchema } from '@baserow/modules/core/utils/schema'
|
||||
|
||||
|
@ -311,7 +311,7 @@ export class PageParameterDataProviderType extends DataProviderType {
|
|||
await Promise.all(
|
||||
pageParams.map(({ name, type }) => {
|
||||
const validators = queryParamNames.includes(name)
|
||||
? QUERY_PARAM_TYPE_VALIDATION_FUNCTIONS
|
||||
? QUERY_PARAM_TYPE_HANDLER_FUNCTIONS
|
||||
: PAGE_PARAM_TYPE_VALIDATION_FUNCTIONS
|
||||
let value
|
||||
try {
|
||||
|
|
|
@ -1497,17 +1497,45 @@ export class ChoiceElementType extends FormElementType {
|
|||
return element.multiple ? 'array' : 'string'
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first option for this element.
|
||||
* @param {Object} element the element we want the option for.
|
||||
* @returns the first option value.
|
||||
*/
|
||||
_getFirstOptionValue(element) {
|
||||
switch (element.option_type) {
|
||||
case CHOICE_OPTION_TYPES.MANUAL:
|
||||
return element.options.find(({ value }) => value)
|
||||
case CHOICE_OPTION_TYPES.FORMULAS: {
|
||||
const formulaValues = ensureArray(
|
||||
this.resolveFormula(this.element.formula_value)
|
||||
)
|
||||
if (formulaValues.length === 0) {
|
||||
return null
|
||||
}
|
||||
return ensureStringOrInteger(formulaValues[0])
|
||||
}
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
getInitialFormDataValue(element, applicationContext) {
|
||||
try {
|
||||
const firstValue = this._getFirstOptionValue(element)
|
||||
let converter = ensureStringOrInteger
|
||||
if (firstValue ?? Number.isInteger(firstValue)) {
|
||||
converter = (v) => ensurePositiveInteger(v, { allowNull: true })
|
||||
}
|
||||
if (element.multiple) {
|
||||
return ensureArray(
|
||||
this.resolveFormula(element.default_value, {
|
||||
element,
|
||||
...applicationContext,
|
||||
})
|
||||
).map(ensureStringOrInteger)
|
||||
).map(converter)
|
||||
} else {
|
||||
return ensureStringOrInteger(
|
||||
return converter(
|
||||
this.resolveFormula(element.default_value, {
|
||||
element,
|
||||
...applicationContext,
|
||||
|
|
|
@ -2,6 +2,8 @@ import {
|
|||
ensureString,
|
||||
ensureNonEmptyString,
|
||||
ensurePositiveInteger,
|
||||
ensureArray,
|
||||
ensureNumeric,
|
||||
} from '@baserow/modules/core/utils/validator'
|
||||
import {
|
||||
DataSourceDataProviderType,
|
||||
|
@ -23,9 +25,20 @@ export const PAGE_PARAM_TYPE_VALIDATION_FUNCTIONS = {
|
|||
numeric: ensurePositiveInteger,
|
||||
text: ensureNonEmptyString,
|
||||
}
|
||||
export const QUERY_PARAM_TYPE_VALIDATION_FUNCTIONS = {
|
||||
numeric: (n) => ensurePositiveInteger(n, { allowNull: true }),
|
||||
text: ensureString,
|
||||
export const QUERY_PARAM_TYPE_HANDLER_FUNCTIONS = {
|
||||
numeric: (input) => {
|
||||
const value = ensureArray(input, { allowEmpty: true }).map((i) =>
|
||||
ensureNumeric(i, { allowNull: true })
|
||||
)
|
||||
|
||||
return value.length === 0 ? null : value.length === 1 ? value[0] : value
|
||||
},
|
||||
text: (input) => {
|
||||
const value = ensureArray(input, {
|
||||
allowEmpty: true,
|
||||
}).map((i) => ensureString(i, { allowEmpty: true }))
|
||||
return value.length === 0 ? null : value.length === 1 ? value[0] : value
|
||||
},
|
||||
}
|
||||
|
||||
export const ALLOWED_LINK_PROTOCOLS = [
|
||||
|
|
|
@ -27,7 +27,7 @@ import {
|
|||
userSourceCookieTokenName,
|
||||
setToken,
|
||||
} from '@baserow/modules/core/utils/auth'
|
||||
import { QUERY_PARAM_TYPE_VALIDATION_FUNCTIONS } from '@baserow/modules/builder/enums'
|
||||
import { QUERY_PARAM_TYPE_HANDLER_FUNCTIONS } from '@baserow/modules/builder/enums'
|
||||
|
||||
const logOffAndReturnToLogin = async ({ builder, store, redirect }) => {
|
||||
await store.dispatch('userSourceUser/logoff', {
|
||||
|
@ -41,6 +41,7 @@ const logOffAndReturnToLogin = async ({ builder, store, redirect }) => {
|
|||
}
|
||||
|
||||
export default {
|
||||
name: 'PublicPage',
|
||||
components: { PageContent, Toasts },
|
||||
provide() {
|
||||
return {
|
||||
|
@ -350,12 +351,11 @@ export default {
|
|||
// update the page's query parameters in the store
|
||||
Promise.all(
|
||||
this.currentPage.query_params.map(({ name, type }) => {
|
||||
if (!newQuery[name]) return null
|
||||
let value
|
||||
try {
|
||||
value = QUERY_PARAM_TYPE_VALIDATION_FUNCTIONS[type](
|
||||
newQuery[name]
|
||||
)
|
||||
if (newQuery[name]) {
|
||||
value = QUERY_PARAM_TYPE_HANDLER_FUNCTIONS[type](newQuery[name])
|
||||
}
|
||||
} catch {
|
||||
// Skip setting the parameter if the user-provided value
|
||||
// doesn't pass our parameter `type` validation.
|
||||
|
|
|
@ -4,10 +4,35 @@ import { trueValues, falseValues } from '@baserow/modules/core/utils/constants'
|
|||
import { DATE_FORMATS } from '@baserow/modules/builder/enums'
|
||||
import moment from '@baserow/modules/core/moment'
|
||||
|
||||
/**
|
||||
* Ensures that the value is a Numeral or can be converted to a numeric value.
|
||||
* @param {number|string} value - The value to ensure as a number.
|
||||
* @param allowNull {boolean} - Whether to allow null or empty values.
|
||||
* @returns {number|null} The value as a Number if conversion is successful.
|
||||
* @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 === '')) {
|
||||
return null
|
||||
}
|
||||
if (Number.isFinite(value)) {
|
||||
return value
|
||||
}
|
||||
if (typeof value === 'string' || value instanceof String) {
|
||||
if (/^([-+])?(\d+(\.\d+)?)$/.test(value)) {
|
||||
return Number(value)
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`Value '${value}' is not a valid number or convertible to a number.`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the value is an integer or can be converted to an integer.
|
||||
* @param {number|string} value - The value to ensure as an integer.
|
||||
* @returns {number} The value as an integer if conversion is successful.
|
||||
* @param allowNull {boolean} - Whether to allow null or empty values.
|
||||
* @returns {number|null} The value as an integer if conversion is successful, null otherwise.
|
||||
* @throws {Error} If the value is not a valid integer or convertible to an integer.
|
||||
*/
|
||||
export const ensureInteger = (value) => {
|
||||
|
@ -34,7 +59,7 @@ export const ensureInteger = (value) => {
|
|||
* @throws {Error} If the value is not a valid non-negative integer
|
||||
*/
|
||||
export const ensurePositiveInteger = (value, { allowNull = false } = {}) => {
|
||||
if (allowNull && value === null) {
|
||||
if (allowNull && (value === null || value === '')) {
|
||||
return null
|
||||
}
|
||||
const validInteger = ensureInteger(value)
|
||||
|
|
|
@ -3,11 +3,15 @@ import {
|
|||
ensureDateTime,
|
||||
ensureInteger,
|
||||
ensureString,
|
||||
ensureNumeric,
|
||||
ensureStringOrInteger,
|
||||
ensurePositiveInteger,
|
||||
} from '@baserow/modules/core/utils/validator'
|
||||
import { expect } from '@jest/globals'
|
||||
import { DATE_FORMATS } from '@baserow/modules/builder/enums'
|
||||
import {
|
||||
DATE_FORMATS,
|
||||
QUERY_PARAM_TYPE_HANDLER_FUNCTIONS,
|
||||
} from '@baserow/modules/builder/enums'
|
||||
|
||||
describe('ensureInteger', () => {
|
||||
it('should return the value as an integer if it is already an integer', () => {
|
||||
|
@ -29,6 +33,130 @@ describe('ensureInteger', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('ensurePositiveInteger', () => {
|
||||
// Valid positive integers
|
||||
test('returns the same value for positive integers', () => {
|
||||
expect(ensurePositiveInteger(5)).toBe(5)
|
||||
expect(ensurePositiveInteger(0)).toBe(0)
|
||||
expect(ensurePositiveInteger(1000)).toBe(1000)
|
||||
})
|
||||
|
||||
// String conversions
|
||||
test('converts string representations of positive integers', () => {
|
||||
expect(ensurePositiveInteger('42')).toBe(42)
|
||||
expect(ensurePositiveInteger('0')).toBe(0)
|
||||
expect(ensurePositiveInteger('+123')).toBe(123)
|
||||
})
|
||||
|
||||
// Null handling
|
||||
test('returns null when value is null and allowNull is true', () => {
|
||||
expect(ensurePositiveInteger(null, { allowNull: true })).toBeNull()
|
||||
})
|
||||
|
||||
test('returns null when value is empty string and allowNull is true', () => {
|
||||
expect(ensurePositiveInteger('', { allowNull: true })).toBeNull()
|
||||
})
|
||||
|
||||
// Error cases
|
||||
test('throws error for negative integers', () => {
|
||||
expect(() => ensurePositiveInteger(-5)).toThrow(
|
||||
'Value is not a positive integer.'
|
||||
)
|
||||
expect(() => ensurePositiveInteger('-10')).toThrow(
|
||||
'Value is not a positive integer.'
|
||||
)
|
||||
})
|
||||
|
||||
test('throws error for non-integer values', () => {
|
||||
expect(() => ensurePositiveInteger('abc')).toThrow('not a valid integer')
|
||||
expect(() => ensurePositiveInteger({})).toThrow('not a valid integer')
|
||||
expect(() => ensurePositiveInteger([])).toThrow('not a valid integer')
|
||||
expect(() => ensurePositiveInteger(3.14)).toThrow('not a valid integer')
|
||||
expect(() => ensurePositiveInteger('3.14')).toThrow('not a valid integer')
|
||||
})
|
||||
|
||||
test('throws error for null when allowNull is false', () => {
|
||||
expect(() => ensurePositiveInteger(null)).toThrow('not a valid integer')
|
||||
})
|
||||
|
||||
test('throws error for empty string when allowNull is false', () => {
|
||||
expect(() => ensurePositiveInteger('')).toThrow('not a valid integer')
|
||||
})
|
||||
|
||||
// Default behavior
|
||||
test('allowNull defaults to false', () => {
|
||||
expect(() => ensurePositiveInteger(null)).toThrow()
|
||||
expect(() => ensurePositiveInteger('')).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('ensureNumeric', () => {
|
||||
// Test valid numeric inputs
|
||||
test('returns the same value for valid numbers', () => {
|
||||
expect(ensureNumeric(42)).toBe(42)
|
||||
expect(ensureNumeric(0)).toBe(0)
|
||||
expect(ensureNumeric(-10)).toBe(-10)
|
||||
expect(ensureNumeric(3.14)).toBe(3.14)
|
||||
})
|
||||
|
||||
// Test string conversion
|
||||
test('converts valid numeric strings to numbers', () => {
|
||||
expect(ensureNumeric('42')).toBe(42)
|
||||
expect(ensureNumeric('0')).toBe(0)
|
||||
expect(ensureNumeric('-10')).toBe(-10)
|
||||
expect(ensureNumeric('3.14')).toBe(3.14)
|
||||
expect(ensureNumeric('+42')).toBe(42)
|
||||
})
|
||||
|
||||
// Test null handling
|
||||
test('handles null values based on allowNull option', () => {
|
||||
expect(() => ensureNumeric(null)).toThrow()
|
||||
expect(ensureNumeric(null, { allowNull: true })).toBeNull()
|
||||
})
|
||||
|
||||
// Test empty string handling
|
||||
test('handles empty strings based on allowNull option', () => {
|
||||
expect(() => ensureNumeric('')).toThrow()
|
||||
expect(ensureNumeric('', { allowNull: true })).toBeNull()
|
||||
})
|
||||
|
||||
// Test invalid inputs
|
||||
test('throws error for non-numeric strings', () => {
|
||||
expect(() => ensureNumeric('abc')).toThrow()
|
||||
expect(() => ensureNumeric('123abc')).toThrow()
|
||||
expect(() => ensureNumeric('abc123')).toThrow()
|
||||
expect(() => ensureNumeric('12.34.56')).toThrow()
|
||||
expect(() => ensureNumeric('Infinity')).toThrow()
|
||||
expect(() => ensureNumeric('-Infinity')).toThrow()
|
||||
})
|
||||
|
||||
test('throws error for objects and arrays', () => {
|
||||
expect(() => ensureNumeric({})).toThrow()
|
||||
expect(() => ensureNumeric([])).toThrow()
|
||||
expect(() => ensureNumeric([1, 2, 3])).toThrow()
|
||||
})
|
||||
|
||||
test('throws error for boolean values', () => {
|
||||
expect(() => ensureNumeric(true)).toThrow()
|
||||
expect(() => ensureNumeric(false)).toThrow()
|
||||
})
|
||||
|
||||
test('throws error for undefined', () => {
|
||||
expect(() => ensureNumeric(undefined)).toThrow()
|
||||
expect(() => ensureNumeric(undefined, { allowNull: true })).toThrow()
|
||||
})
|
||||
|
||||
// Test error message
|
||||
test('provides descriptive error message', () => {
|
||||
try {
|
||||
ensureNumeric('not-a-number')
|
||||
} catch (error) {
|
||||
expect(error.message).toContain('not-a-number')
|
||||
expect(error.message).toContain('not a valid number')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('ensureString', () => {
|
||||
it('should return an empty string if the value is falsy', () => {
|
||||
expect(ensureString(null)).toBe('')
|
||||
|
@ -185,3 +313,68 @@ describe('ensureDateTime', () => {
|
|||
).toBe(date)
|
||||
})
|
||||
})
|
||||
|
||||
describe('QUERY_PARAM_TYPE_HANDLER_FUNCTIONS', () => {
|
||||
describe('numeric handler', () => {
|
||||
const numericHandler = QUERY_PARAM_TYPE_HANDLER_FUNCTIONS.numeric
|
||||
|
||||
it('should return null for empty input', () => {
|
||||
expect(numericHandler('')).toBe(null)
|
||||
expect(numericHandler(null)).toBe(null)
|
||||
expect(numericHandler(undefined)).toBe(null)
|
||||
})
|
||||
|
||||
it('should handle single numeric string', () => {
|
||||
expect(numericHandler('5')).toBe(5)
|
||||
expect(numericHandler('42')).toBe(42)
|
||||
expect(numericHandler('1.5')).toBe(1.5)
|
||||
expect(numericHandler('-1')).toBe(-1)
|
||||
})
|
||||
|
||||
it('should handle comma-separated numeric strings', () => {
|
||||
expect(numericHandler('1,2,3')).toEqual([1, 2, 3])
|
||||
expect(numericHandler('42,123')).toEqual([42, 123])
|
||||
})
|
||||
|
||||
it('should filter out invalid values from arrays', () => {
|
||||
expect(numericHandler('1,2,')).toEqual([1, 2, null])
|
||||
expect(numericHandler('1,2')).toEqual([1, 2])
|
||||
expect(numericHandler(',1,2')).toEqual([null, 1, 2])
|
||||
expect(numericHandler('1,,2')).toEqual([1, null, 2])
|
||||
})
|
||||
|
||||
it('should throw error for invalid numeric values', () => {
|
||||
expect(() => numericHandler('1,abc,3')).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('text handler', () => {
|
||||
const textHandler = QUERY_PARAM_TYPE_HANDLER_FUNCTIONS.text
|
||||
|
||||
it('should return null for empty input', () => {
|
||||
expect(textHandler('')).toBe(null)
|
||||
expect(textHandler(null)).toBe(null)
|
||||
expect(textHandler(undefined)).toBe(null)
|
||||
})
|
||||
|
||||
it('should handle single text value', () => {
|
||||
expect(textHandler('hello')).toBe('hello')
|
||||
expect(textHandler('test123')).toBe('test123')
|
||||
})
|
||||
|
||||
it('should handle comma-separated text values', () => {
|
||||
expect(textHandler('hello,world')).toEqual(['hello', 'world'])
|
||||
expect(textHandler('a,b,c')).toEqual(['a', 'b', 'c'])
|
||||
})
|
||||
|
||||
it('should filter out empty values from arrays', () => {
|
||||
expect(textHandler('test,')).toEqual(['test', ''])
|
||||
expect(textHandler('test,,value')).toEqual(['test', '', 'value'])
|
||||
})
|
||||
|
||||
it('should handle numeric values as strings', () => {
|
||||
expect(textHandler('123')).toBe('123')
|
||||
expect(textHandler('test,123,abc')).toEqual(['test', '123', 'abc'])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Add table
Reference in a new issue