mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-06 14:05:28 +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:
|
if service_filter.value_is_formula:
|
||||||
try:
|
try:
|
||||||
resolved_value = str(
|
resolved_value = ensure_string(
|
||||||
resolve_formula(
|
resolve_formula(
|
||||||
service_filter.value,
|
service_filter.value,
|
||||||
formula_runtime_function_registry,
|
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.rows.handler import RowHandler
|
||||||
from baserow.contrib.database.table.handler import TableHandler
|
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.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.models import LocalBaserowListRows
|
||||||
from baserow.contrib.integrations.local_baserow.service_types import (
|
from baserow.contrib.integrations.local_baserow.service_types import (
|
||||||
LocalBaserowListRowsUserServiceType,
|
LocalBaserowListRowsUserServiceType,
|
||||||
|
@ -1162,3 +1163,63 @@ def test_extract_properties(path, expected):
|
||||||
result = service_type.extract_properties(path)
|
result = service_type.extract_properties(path)
|
||||||
|
|
||||||
assert result == expected
|
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.describe("Builder page test suite", () => {
|
||||||
test.beforeEach(async ({builderPagePage}) => {
|
test.beforeEach(async ({ builderPagePage }) => {
|
||||||
await builderPagePage.goto();
|
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("New page").click();
|
||||||
await page.getByText("Create page").waitFor();
|
await page.getByText("Create page").waitFor();
|
||||||
await page
|
await page
|
||||||
|
@ -25,12 +25,12 @@ test.describe("Builder page test suite", () => {
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Can open page settings", async ({page}) => {
|
test("Can open page settings", async ({ page }) => {
|
||||||
await page.getByText("Page settings").click();
|
await page.getByText("Page settings").click();
|
||||||
await expect(page.locator(".box__title").getByText("Page")).toBeVisible();
|
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.getByText("Page settings").click();
|
||||||
|
|
||||||
await page
|
await page
|
||||||
|
@ -57,9 +57,9 @@ test.describe("Builder page test suite", () => {
|
||||||
).toBeVisible();
|
).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("Click to create an element").click();
|
||||||
await page.getByText("Heading", {exact: true}).click();
|
await page.getByText("Heading", { exact: true }).click();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.locator(".modal__box").getByText("Add new element")
|
page.locator(".modal__box").getByText("Add new element")
|
||||||
|
@ -69,13 +69,13 @@ test.describe("Builder page test suite", () => {
|
||||||
).toBeVisible();
|
).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(".header").getByText("Elements").click();
|
||||||
await page
|
await page
|
||||||
.locator(".elements-context")
|
.locator(".elements-context")
|
||||||
.getByText("Element", {exact: true})
|
.getByText("Element", { exact: true })
|
||||||
.click();
|
.click();
|
||||||
await page.getByText("Heading", {exact: true}).click();
|
await page.getByText("Heading", { exact: true }).click();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.locator(".modal__box").getByText("Add new element")
|
page.locator(".modal__box").getByText("Add new element")
|
||||||
|
@ -85,8 +85,7 @@ test.describe("Builder page test suite", () => {
|
||||||
).toBeVisible();
|
).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.getByText("Page settings").click();
|
||||||
|
|
||||||
await page
|
await page
|
||||||
|
@ -94,13 +93,33 @@ test.describe("Builder page test suite", () => {
|
||||||
.getByPlaceholder("Enter a name...")
|
.getByPlaceholder("Enter a name...")
|
||||||
.fill("New page 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
|
await page
|
||||||
.locator(".page-settings-query-params .form-input__wrapper")
|
.locator(".page-settings-query-params .form-input__wrapper")
|
||||||
.getByRole('textbox')
|
.getByRole("textbox")
|
||||||
|
.nth(1)
|
||||||
.fill("my_param");
|
.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 page.locator(".button").getByText("Save").click();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText("The page settings have been updated.")
|
page.getByText("The page settings have been updated.")
|
||||||
|
@ -108,20 +127,33 @@ test.describe("Builder page test suite", () => {
|
||||||
|
|
||||||
await page.getByTitle("Close").click();
|
await page.getByTitle("Close").click();
|
||||||
await expect(page.locator(".box__title").getByText("Page")).toBeHidden();
|
await expect(page.locator(".box__title").getByText("Page")).toBeHidden();
|
||||||
await page.getByText('Click to create an element').click();
|
await page.getByText("Click to create an element").click();
|
||||||
await page.locator('.add-element-card__element-type-icon-link').click();
|
await page.locator(".add-element-card__element-type-icon-link").click();
|
||||||
await page.getByLabel('my_param=').fill("foo")
|
await page.getByLabel("my_param=").fill("foo");
|
||||||
await page.getByRole('complementary').getByRole('textbox').click();
|
await page.getByLabel("my_param2=").fill("15");
|
||||||
await page.getByRole('complementary').getByRole('textbox').locator('div').first().fill('linkim');
|
await page.getByRole("complementary").getByRole("textbox").click();
|
||||||
await page.locator('a').filter({hasText: 'Make a choice'}).click();
|
await page
|
||||||
await page.locator('a').filter({hasText: '?my_param=*'}).click();
|
.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
|
// click empty place to close tooltip from prev. step
|
||||||
await page.click('body')
|
await page.click("body");
|
||||||
await page.getByRole('textbox').nth(2).click();
|
await page.getByRole("textbox").nth(4).click();
|
||||||
await page.getByText('my_param', { exact: true }).first().click();
|
await page.getByText("my_param", { exact: true }).first().click();
|
||||||
await page.click('body')
|
await page.locator('[id="__layout"]').getByRole("textbox").nth(3).click();
|
||||||
await expect(page.getByRole('link', {name: 'linkim'})).toHaveAttribute(
|
await page.getByText("my_param2", { exact: true }).first().click();
|
||||||
'href', /\?my_param=foo/);
|
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,
|
ensureString,
|
||||||
ensureStringOrInteger,
|
ensureStringOrInteger,
|
||||||
ensureArray,
|
ensureArray,
|
||||||
|
ensurePositiveInteger,
|
||||||
} from '@baserow/modules/core/utils/validator'
|
} from '@baserow/modules/core/utils/validator'
|
||||||
import { CHOICE_OPTION_TYPES } from '@baserow/modules/builder/enums'
|
import { CHOICE_OPTION_TYPES } from '@baserow/modules/builder/enums'
|
||||||
|
|
||||||
|
@ -94,20 +95,36 @@ export default {
|
||||||
return ensureString(this.resolveFormula(this.element.placeholder))
|
return ensureString(this.resolveFormula(this.element.placeholder))
|
||||||
},
|
},
|
||||||
defaultValueResolved() {
|
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) {
|
if (this.element.multiple) {
|
||||||
const existingValues = this.optionsResolved.map(({ value }) => value)
|
try {
|
||||||
return ensureArray(this.resolveFormula(this.element.default_value))
|
const existingValues = this.optionsResolved.map(({ value }) => value)
|
||||||
.map(ensureStringOrInteger)
|
return ensureArray(this.resolveFormula(this.element.default_value))
|
||||||
.filter((value) => existingValues.includes(value))
|
.map(converter)
|
||||||
|
.filter((value) => existingValues.includes(value))
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Always return a string if we have a default value, otherwise
|
try {
|
||||||
// set the value to null as single select fields will only skip
|
// Always return a string if we have a default value, otherwise
|
||||||
// field preparation if the value is null.
|
// set the value to null as single select fields will only skip
|
||||||
const resolvedSingleValue = ensureStringOrInteger(
|
// field preparation if the value is null.
|
||||||
this.resolveFormula(this.element.default_value)
|
const resolvedSingleValue = converter(
|
||||||
)
|
this.resolveFormula(this.element.default_value)
|
||||||
|
)
|
||||||
return resolvedSingleValue === '' ? null : resolvedSingleValue
|
return resolvedSingleValue === '' ? null : resolvedSingleValue
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
canHaveOptions() {
|
canHaveOptions() {
|
||||||
|
|
|
@ -35,21 +35,22 @@
|
||||||
class="preview-navigation-bar__query-separator"
|
class="preview-navigation-bar__query-separator"
|
||||||
>
|
>
|
||||||
{{ index === 0 ? '?' : '&' }}
|
{{ index === 0 ? '?' : '&' }}
|
||||||
</span>
|
|
||||||
<PreviewNavigationBarQueryParam
|
<label :for="queryParam.name">{{ queryParam.name }}=</label>
|
||||||
:key="`param-${queryParam.key}`"
|
<PreviewNavigationBarInput
|
||||||
:class="`preview-navigation-bar__query-parameter-input--${queryParam.type}`"
|
:id="queryParam.name"
|
||||||
:validation-fn="queryParam.validationFn"
|
:key="`param-${queryParam.key}`"
|
||||||
:default-value="pageParameters[queryParam.name]"
|
:class="`preview-navigation-bar__query-parameter-input--${queryParam.type}`"
|
||||||
:name="queryParam.name"
|
:validation-fn="queryParam.validationFn"
|
||||||
@change="
|
:default-value="pageParameters[queryParam.name]"
|
||||||
actionSetParameter({
|
@change="
|
||||||
page,
|
actionSetParameter({
|
||||||
name: queryParam.name,
|
page,
|
||||||
value: $event,
|
name: queryParam.name,
|
||||||
})
|
value: $event,
|
||||||
"
|
})
|
||||||
/>
|
"
|
||||||
|
/></span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div />
|
<div />
|
||||||
|
@ -61,15 +62,13 @@ import { splitPath } from '@baserow/modules/builder/utils/path'
|
||||||
import PreviewNavigationBarInput from '@baserow/modules/builder/components/page/PreviewNavigationBarInput'
|
import PreviewNavigationBarInput from '@baserow/modules/builder/components/page/PreviewNavigationBarInput'
|
||||||
import UserSelector from '@baserow/modules/builder/components/page/UserSelector'
|
import UserSelector from '@baserow/modules/builder/components/page/UserSelector'
|
||||||
import { mapActions } from 'vuex'
|
import { mapActions } from 'vuex'
|
||||||
import { PAGE_PARAM_TYPE_VALIDATION_FUNCTIONS } from '@baserow/modules/builder/enums'
|
import {
|
||||||
import PreviewNavigationBarQueryParam from '@baserow/modules/builder/components/page/PreviewNavigationBarQueryParam.vue'
|
PAGE_PARAM_TYPE_VALIDATION_FUNCTIONS,
|
||||||
|
QUERY_PARAM_TYPE_HANDLER_FUNCTIONS,
|
||||||
|
} from '@baserow/modules/builder/enums'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: { PreviewNavigationBarInput, UserSelector },
|
||||||
PreviewNavigationBarInput,
|
|
||||||
UserSelector,
|
|
||||||
PreviewNavigationBarQueryParam,
|
|
||||||
},
|
|
||||||
props: {
|
props: {
|
||||||
page: {
|
page: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -84,7 +83,7 @@ export default {
|
||||||
return this.page.query_params.map((queryParam, idx) => ({
|
return this.page.query_params.map((queryParam, idx) => ({
|
||||||
...queryParam,
|
...queryParam,
|
||||||
key: `query-param-${queryParam.name}-${idx}`,
|
key: `query-param-${queryParam.name}-${idx}`,
|
||||||
validationFn: PAGE_PARAM_TYPE_VALIDATION_FUNCTIONS[queryParam.type],
|
validationFn: QUERY_PARAM_TYPE_HANDLER_FUNCTIONS[queryParam.type],
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
splitPath() {
|
splitPath() {
|
||||||
|
|
|
@ -9,10 +9,12 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
defaultValue: {
|
defaultValue: {
|
||||||
type: [String, Number],
|
type: [String, Number, Array],
|
||||||
required: false,
|
required: false,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
@ -45,7 +47,9 @@ export default {
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
defaultValue(newValue) {
|
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 { DEFAULT_USER_ROLE_PREFIX } from '@baserow/modules/builder/constants'
|
||||||
import {
|
import {
|
||||||
PAGE_PARAM_TYPE_VALIDATION_FUNCTIONS,
|
PAGE_PARAM_TYPE_VALIDATION_FUNCTIONS,
|
||||||
QUERY_PARAM_TYPE_VALIDATION_FUNCTIONS,
|
QUERY_PARAM_TYPE_HANDLER_FUNCTIONS,
|
||||||
} from '@baserow/modules/builder/enums'
|
} from '@baserow/modules/builder/enums'
|
||||||
import { extractSubSchema } from '@baserow/modules/core/utils/schema'
|
import { extractSubSchema } from '@baserow/modules/core/utils/schema'
|
||||||
|
|
||||||
|
@ -311,7 +311,7 @@ export class PageParameterDataProviderType extends DataProviderType {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
pageParams.map(({ name, type }) => {
|
pageParams.map(({ name, type }) => {
|
||||||
const validators = queryParamNames.includes(name)
|
const validators = queryParamNames.includes(name)
|
||||||
? QUERY_PARAM_TYPE_VALIDATION_FUNCTIONS
|
? QUERY_PARAM_TYPE_HANDLER_FUNCTIONS
|
||||||
: PAGE_PARAM_TYPE_VALIDATION_FUNCTIONS
|
: PAGE_PARAM_TYPE_VALIDATION_FUNCTIONS
|
||||||
let value
|
let value
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1497,17 +1497,45 @@ export class ChoiceElementType extends FormElementType {
|
||||||
return element.multiple ? 'array' : 'string'
|
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) {
|
getInitialFormDataValue(element, applicationContext) {
|
||||||
try {
|
try {
|
||||||
|
const firstValue = this._getFirstOptionValue(element)
|
||||||
|
let converter = ensureStringOrInteger
|
||||||
|
if (firstValue ?? Number.isInteger(firstValue)) {
|
||||||
|
converter = (v) => ensurePositiveInteger(v, { allowNull: true })
|
||||||
|
}
|
||||||
if (element.multiple) {
|
if (element.multiple) {
|
||||||
return ensureArray(
|
return ensureArray(
|
||||||
this.resolveFormula(element.default_value, {
|
this.resolveFormula(element.default_value, {
|
||||||
element,
|
element,
|
||||||
...applicationContext,
|
...applicationContext,
|
||||||
})
|
})
|
||||||
).map(ensureStringOrInteger)
|
).map(converter)
|
||||||
} else {
|
} else {
|
||||||
return ensureStringOrInteger(
|
return converter(
|
||||||
this.resolveFormula(element.default_value, {
|
this.resolveFormula(element.default_value, {
|
||||||
element,
|
element,
|
||||||
...applicationContext,
|
...applicationContext,
|
||||||
|
|
|
@ -2,6 +2,8 @@ import {
|
||||||
ensureString,
|
ensureString,
|
||||||
ensureNonEmptyString,
|
ensureNonEmptyString,
|
||||||
ensurePositiveInteger,
|
ensurePositiveInteger,
|
||||||
|
ensureArray,
|
||||||
|
ensureNumeric,
|
||||||
} from '@baserow/modules/core/utils/validator'
|
} from '@baserow/modules/core/utils/validator'
|
||||||
import {
|
import {
|
||||||
DataSourceDataProviderType,
|
DataSourceDataProviderType,
|
||||||
|
@ -23,9 +25,20 @@ export const PAGE_PARAM_TYPE_VALIDATION_FUNCTIONS = {
|
||||||
numeric: ensurePositiveInteger,
|
numeric: ensurePositiveInteger,
|
||||||
text: ensureNonEmptyString,
|
text: ensureNonEmptyString,
|
||||||
}
|
}
|
||||||
export const QUERY_PARAM_TYPE_VALIDATION_FUNCTIONS = {
|
export const QUERY_PARAM_TYPE_HANDLER_FUNCTIONS = {
|
||||||
numeric: (n) => ensurePositiveInteger(n, { allowNull: true }),
|
numeric: (input) => {
|
||||||
text: ensureString,
|
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 = [
|
export const ALLOWED_LINK_PROTOCOLS = [
|
||||||
|
|
|
@ -27,7 +27,7 @@ import {
|
||||||
userSourceCookieTokenName,
|
userSourceCookieTokenName,
|
||||||
setToken,
|
setToken,
|
||||||
} from '@baserow/modules/core/utils/auth'
|
} 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 }) => {
|
const logOffAndReturnToLogin = async ({ builder, store, redirect }) => {
|
||||||
await store.dispatch('userSourceUser/logoff', {
|
await store.dispatch('userSourceUser/logoff', {
|
||||||
|
@ -41,6 +41,7 @@ const logOffAndReturnToLogin = async ({ builder, store, redirect }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
name: 'PublicPage',
|
||||||
components: { PageContent, Toasts },
|
components: { PageContent, Toasts },
|
||||||
provide() {
|
provide() {
|
||||||
return {
|
return {
|
||||||
|
@ -350,12 +351,11 @@ export default {
|
||||||
// update the page's query parameters in the store
|
// update the page's query parameters in the store
|
||||||
Promise.all(
|
Promise.all(
|
||||||
this.currentPage.query_params.map(({ name, type }) => {
|
this.currentPage.query_params.map(({ name, type }) => {
|
||||||
if (!newQuery[name]) return null
|
|
||||||
let value
|
let value
|
||||||
try {
|
try {
|
||||||
value = QUERY_PARAM_TYPE_VALIDATION_FUNCTIONS[type](
|
if (newQuery[name]) {
|
||||||
newQuery[name]
|
value = QUERY_PARAM_TYPE_HANDLER_FUNCTIONS[type](newQuery[name])
|
||||||
)
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Skip setting the parameter if the user-provided value
|
// Skip setting the parameter if the user-provided value
|
||||||
// doesn't pass our parameter `type` validation.
|
// 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 { DATE_FORMATS } from '@baserow/modules/builder/enums'
|
||||||
import moment from '@baserow/modules/core/moment'
|
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.
|
* 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.
|
* @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.
|
* @throws {Error} If the value is not a valid integer or convertible to an integer.
|
||||||
*/
|
*/
|
||||||
export const ensureInteger = (value) => {
|
export const ensureInteger = (value) => {
|
||||||
|
@ -34,7 +59,7 @@ export const ensureInteger = (value) => {
|
||||||
* @throws {Error} If the value is not a valid non-negative integer
|
* @throws {Error} If the value is not a valid non-negative integer
|
||||||
*/
|
*/
|
||||||
export const ensurePositiveInteger = (value, { allowNull = false } = {}) => {
|
export const ensurePositiveInteger = (value, { allowNull = false } = {}) => {
|
||||||
if (allowNull && value === null) {
|
if (allowNull && (value === null || value === '')) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const validInteger = ensureInteger(value)
|
const validInteger = ensureInteger(value)
|
||||||
|
|
|
@ -3,11 +3,15 @@ import {
|
||||||
ensureDateTime,
|
ensureDateTime,
|
||||||
ensureInteger,
|
ensureInteger,
|
||||||
ensureString,
|
ensureString,
|
||||||
|
ensureNumeric,
|
||||||
ensureStringOrInteger,
|
ensureStringOrInteger,
|
||||||
ensurePositiveInteger,
|
ensurePositiveInteger,
|
||||||
} from '@baserow/modules/core/utils/validator'
|
} from '@baserow/modules/core/utils/validator'
|
||||||
import { expect } from '@jest/globals'
|
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', () => {
|
describe('ensureInteger', () => {
|
||||||
it('should return the value as an integer if it is already an integer', () => {
|
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', () => {
|
describe('ensureString', () => {
|
||||||
it('should return an empty string if the value is falsy', () => {
|
it('should return an empty string if the value is falsy', () => {
|
||||||
expect(ensureString(null)).toBe('')
|
expect(ensureString(null)).toBe('')
|
||||||
|
@ -185,3 +313,68 @@ describe('ensureDateTime', () => {
|
||||||
).toBe(date)
|
).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