1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-03-23 16:23:25 +00:00

Resolve "Checkbox element"

This commit is contained in:
Alexander Haller 2024-01-04 13:10:44 +00:00 committed by Jérémie Pardou
parent 283f90e53f
commit af2ff2dd6e
20 changed files with 428 additions and 12 deletions
backend
src/baserow/contrib/builder
tests/baserow/contrib/builder/elements
web-frontend/modules

View file

@ -158,6 +158,7 @@ class BuilderConfig(AppConfig):
from .elements.element_types import (
ButtonElementType,
CheckboxElementType,
ColumnElementType,
DropdownElementType,
FormContainerElementType,
@ -180,6 +181,7 @@ class BuilderConfig(AppConfig):
element_type_registry.register(TableElementType())
element_type_registry.register(FormContainerElementType())
element_type_registry.register(DropdownElementType())
element_type_registry.register(CheckboxElementType())
from .domains.domain_types import CustomDomainType, SubDomainType
from .domains.registries import domain_type_registry

View file

@ -15,6 +15,7 @@ from baserow.contrib.builder.elements.handler import ElementHandler
from baserow.contrib.builder.elements.models import (
WIDTHS,
ButtonElement,
CheckboxElement,
CollectionField,
ColumnElement,
ContainerElement,
@ -1000,6 +1001,64 @@ class FormContainerElementType(ContainerElementType):
return super().import_serialized(page, serialized_copy, id_mapping)
class CheckboxElementType(InputElementType):
type = "checkbox"
model_class = CheckboxElement
allowed_fields = ["label", "default_value", "required"]
serializer_field_names = ["label", "default_value", "required"]
class SerializedDict(ElementDict):
label: BaserowFormula
required: bool
default_value: BaserowFormula
@property
def serializer_field_overrides(self):
from baserow.core.formula.serializers import FormulaSerializerField
overrides = {
"label": FormulaSerializerField(
help_text=CheckboxElement._meta.get_field("label").help_text,
required=False,
allow_blank=True,
default="",
),
"default_value": FormulaSerializerField(
help_text=CheckboxElement._meta.get_field("default_value").help_text,
required=False,
allow_blank=True,
default="",
),
"required": serializers.BooleanField(
help_text=CheckboxElement._meta.get_field("required").help_text,
default=False,
required=False,
),
}
return overrides
def import_serialized(self, page, serialized_values, id_mapping):
serialized_copy = serialized_values.copy()
if serialized_copy["label"]:
serialized_copy["label"] = import_formula(
serialized_copy["label"], id_mapping
)
if serialized_copy["default_value"]:
serialized_copy["default_value"] = import_formula(
serialized_copy["default_value"], id_mapping
)
return super().import_serialized(page, serialized_copy, id_mapping)
def get_pytest_params(self, pytest_data_fixture):
return {
"label": "",
"required": False,
"default_value": "",
}
class DropdownElementType(FormElementType):
type = "dropdown"
model_class = DropdownElement

View file

@ -628,3 +628,18 @@ class DropdownElementOption(models.Model):
blank=True, default="", help_text="The display name of the option"
)
dropdown = models.ForeignKey(DropdownElement, on_delete=models.CASCADE)
class CheckboxElement(InputElement):
"""
A checkbox element.
"""
label = FormulaField(
default="",
help_text="The text label for this input",
)
default_value = FormulaField(default="", help_text="The input's default value.")
required = models.BooleanField(
default=False, help_text="Whether this input is a required field."
)

View file

@ -0,0 +1,54 @@
# Generated by Django 3.2.21 on 2023-12-12 13:16
import django.db.models.deletion
from django.db import migrations, models
import baserow.core.formula.field
class Migration(migrations.Migration):
dependencies = [
("builder", "0035_add_more_styles"),
]
operations = [
migrations.CreateModel(
name="CheckboxElement",
fields=[
(
"element_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="builder.element",
),
),
(
"label",
baserow.core.formula.field.FormulaField(
default="", help_text="The text label for this input"
),
),
(
"default_value",
baserow.core.formula.field.FormulaField(
default="", help_text="The input's default value."
),
),
(
"required",
models.BooleanField(
default=False,
help_text="Whether this input is a required field.",
),
),
],
options={
"abstract": False,
},
bases=("builder.element",),
),
]

View file

@ -4,6 +4,7 @@ import pytest
from rest_framework.exceptions import ValidationError
from baserow.contrib.builder.elements.element_types import (
CheckboxElementType,
ContainerElementType,
DropdownElementType,
FormElementType,
@ -11,6 +12,7 @@ from baserow.contrib.builder.elements.element_types import (
)
from baserow.contrib.builder.elements.handler import ElementHandler
from baserow.contrib.builder.elements.models import (
CheckboxElement,
DropdownElementOption,
HeadingElement,
InputTextElement,
@ -215,3 +217,27 @@ def test_page_with_element_using_form_data_has_dependencies_import_first(data_fi
form_input_clone = InputTextElement.objects.get(page=page_clone)
heading_clone = HeadingElement.objects.get(page=page_clone)
assert heading_clone.value == f"get('form_data.{form_input_clone.id}')"
@pytest.mark.django_db
def test_checkbox_element_import_export_formula(data_fixture):
page = data_fixture.create_builder_page()
data_source_1 = data_fixture.create_builder_local_baserow_get_row_data_source()
data_source_2 = data_fixture.create_builder_local_baserow_get_row_data_source()
element_type = CheckboxElementType()
exported_input_element = data_fixture.create_builder_element(
CheckboxElement,
label=f"get('data_source.{data_source_1.id}.field_1')",
default_value=f"get('data_source.{data_source_1.id}.field_1')",
)
serialized = element_type.export_serialized(exported_input_element)
# After applying the ID mapping the imported formula should have updated
# the data source IDs
id_mapping = {"builder_data_sources": {data_source_1.id: data_source_2.id}}
imported_element = element_type.import_serialized(page, serialized, id_mapping)
expected_formula = f"get('data_source.{data_source_2.id}.field_1')"
assert imported_element.label == expected_formula
assert imported_element.default_value == expected_formula

View file

@ -0,0 +1,64 @@
<template>
<div class="checkbox-element">
<input
class="checkbox-element__input"
type="checkbox"
:checked="value"
:required="element.required"
:disabled="isEditMode"
@change="toggleValue"
/>
<label
v-if="resolvedLabel"
class="checkbox-element__label"
@click="toggleValue"
>
{{ resolvedLabel }}
</label>
</div>
</template>
<script>
import formElement from '@baserow/modules/builder/mixins/formElement'
import { ensureBoolean } from '@baserow/modules/core/utils/validator'
export default {
name: 'CheckboxElement',
mixins: [formElement],
data() {
return {
value: false,
}
},
computed: {
defaultValueResolved() {
try {
return ensureBoolean(this.resolveFormula(this.element.default_value))
} catch {
return false
}
},
resolvedLabel() {
return this.resolveFormula(this.element.label)
},
},
watch: {
defaultValueResolved: {
handler(value) {
this.value = value
},
immediate: true,
},
value(value) {
this.setFormData(value)
},
},
methods: {
toggleValue() {
if (!this.isEditMode) {
this.value = !this.value
}
},
},
}
</script>

View file

@ -6,7 +6,7 @@
<input
type="text"
class="input-element"
:readonly="isEditable"
:readonly="isEditMode"
:value="resolvedDefaultValue"
:required="element.required"
:placeholder="resolvedPlaceholder"

View file

@ -0,0 +1,65 @@
<template>
<form @submit.prevent @keydown.enter.prevent>
<ApplicationBuilderFormulaInputGroup
v-model="values.label"
:label="$t('checkboxElementForm.labelTitle')"
:placeholder="$t('generalForm.labelPlaceholder')"
:data-providers-allowed="dataProvidersAllowed"
></ApplicationBuilderFormulaInputGroup>
<ApplicationBuilderFormulaInputGroup
v-model="values.default_value"
:label="$t('checkboxElementForm.valueTitle')"
:placeholder="$t('generalForm.valuePlaceholder')"
:data-providers-allowed="dataProvidersAllowed"
></ApplicationBuilderFormulaInputGroup>
<FormElement class="control">
<label class="control__label">
{{ $t('checkboxElementForm.requiredTitle') }}
</label>
<div class="control__elements">
<Checkbox v-model="values.required"></Checkbox>
</div>
</FormElement>
</form>
</template>
<script>
import form from '@baserow/modules/core/mixins/form'
import ApplicationBuilderFormulaInputGroup from '@baserow/modules/builder/components/ApplicationBuilderFormulaInputGroup.vue'
import {
CurrentRecordDataProviderType,
DataSourceDataProviderType,
PageParameterDataProviderType,
} from '@baserow/modules/builder/dataProviderTypes'
export default {
name: 'CheckboxElementForm',
components: { ApplicationBuilderFormulaInputGroup },
mixins: [form],
data() {
return {
values: {
label: '',
default_value: '',
required: false,
},
}
},
computed: {
dataProvidersAllowed() {
return [
CurrentRecordDataProviderType.getType(),
PageParameterDataProviderType.getType(),
DataSourceDataProviderType.getType(),
]
},
},
methods: {
emitChange(newValues) {
if (this.isFormValid()) {
form.methods.emitChange.bind(this)(newValues)
}
},
},
}
</script>

View file

@ -16,6 +16,7 @@ import {
ELEMENT_EVENTS,
PAGE_PARAM_TYPE_VALIDATION_FUNCTIONS,
} from '@baserow/modules/builder/enums'
import { ensureBoolean } from '@baserow/modules/core/utils/validator'
import ColumnElement from '@baserow/modules/builder/components/elements/components/ColumnElement'
import ColumnElementForm from '@baserow/modules/builder/components/elements/components/forms/general/ColumnElementForm'
import _ from 'lodash'
@ -30,6 +31,8 @@ import FormContainerElement from '@baserow/modules/builder/components/elements/c
import FormContainerElementForm from '@baserow/modules/builder/components/elements/components/forms/general/FormContainerElementForm.vue'
import DropdownElement from '@baserow/modules/builder/components/elements/components/DropdownElement.vue'
import DropdownElementForm from '@baserow/modules/builder/components/elements/components/forms/general/DropdownElementForm.vue'
import CheckboxElement from '@baserow/modules/builder/components/elements/components/CheckboxElement.vue'
import CheckboxElementForm from '@baserow/modules/builder/components/elements/components/forms/general/CheckboxElementForm.vue'
export class ElementType extends Registerable {
get name() {
@ -654,3 +657,46 @@ export class FormContainerElementType extends ContainerElementType {
return ['style_width']
}
}
export class CheckboxElementType extends FormElementType {
getType() {
return 'checkbox'
}
get name() {
return this.app.i18n.t('elementType.checkbox')
}
get description() {
return this.app.i18n.t('elementType.checkboxDescription')
}
get iconClass() {
return 'iconoir-check'
}
get component() {
return CheckboxElement
}
get generalFormComponent() {
return CheckboxElementForm
}
get formDataType() {
return 'boolean'
}
getInitialFormDataValue(element, applicationContext) {
try {
return ensureBoolean(
this.resolveFormula(element.default_value, {
element,
...applicationContext,
})
)
} catch {
return false
}
}
}

View file

@ -85,7 +85,9 @@
"formContainer": "Form",
"formContainerDescription": "A form element",
"dropdown": "Dropdown",
"dropdownDescription": "Dropdown element"
"dropdownDescription": "Dropdown element",
"checkbox": "Checkbox",
"checkboxDescription": "Checkbox element"
},
"addElementButton": {
"label": "Element"
@ -430,5 +432,10 @@
"rowIdPlaceholder": "Select a row ID",
"fieldMappingPlaceholder": "Choose a field value",
"noTableSelectedMessage": "Choose a table to begin configuring your fields."
},
"checkboxElementForm": {
"labelTitle": "Label",
"valueTitle": "Default value",
"requiredTitle": "Required"
}
}

View file

@ -21,7 +21,7 @@ export default {
elementType() {
return this.$registry.get('element', this.element.type)
},
isEditable() {
isEditMode() {
return this.mode === 'editing'
},
applicationContext() {

View file

@ -36,6 +36,7 @@ import {
TableElementType,
FormContainerElementType,
DropdownElementType,
CheckboxElementType,
} from '@baserow/modules/builder/elementTypes'
import {
DesktopDeviceType,
@ -167,6 +168,7 @@ export default (context) => {
app.$registry.register('element', new TableElementType(context))
app.$registry.register('element', new FormContainerElementType(context))
app.$registry.register('element', new DropdownElementType(context))
app.$registry.register('element', new CheckboxElementType(context))
app.$registry.register('device', new DesktopDeviceType(context))
app.$registry.register('device', new TabletDeviceType(context))

View file

@ -9,3 +9,4 @@
@import 'button_element';
@import 'table_element';
@import 'baserow_table';
@import 'checkbox_element';

View file

@ -0,0 +1,15 @@
.checkbox-element {
font-size: 14px;
}
.checkbox-element__input {
cursor: pointer;
transform: scale(1.4);
margin-right: 10px;
}
.checkbox-element__label {
cursor: pointer;
color: $black;
user-select: none;
}

View file

@ -0,0 +1,45 @@
// List of values considered as true.
// Compatible with backend/src/baserow/contrib/database/fields/constants.py
export const trueValues = [
't',
'T',
'y',
'Y',
'yes',
'Yes',
'YES',
'true',
'True',
'TRUE',
'o', // This one is not on the backend but was on the frontend
'on',
'On',
'ON',
'1',
1,
'checked',
true,
]
// List of values considered as false.
// Compatible with backend/src/baserow/contrib/database/fields/constants.py
export const falseValues = [
'f',
'F',
'n',
'N',
'no',
'No',
'NO',
'false',
'False',
'FALSE',
'off',
'Off',
'OFF',
'0',
0,
0.0,
'unchecked',
false,
]

View file

@ -1,3 +1,5 @@
import { trueValues, falseValues } from '@baserow/modules/core/utils/constants'
/**
* 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.
@ -17,7 +19,7 @@ export const ensureInteger = (value) => {
}
/**
* Ensures that the value is a string or convert it.
* Ensures that the value is a string or try to convert it.
* @param {*} value - The value to ensure as a string.
* @returns {string} The value as a string.
*/
@ -27,3 +29,17 @@ export const ensureString = (value) => {
}
return `${value}`
}
/**
* Ensures that the value is a boolean or convert it.
* @param {*} value - The value to ensure as a boolean.
* @returns {boolean} The value as a boolean.
*/
export const ensureBoolean = (value) => {
if (trueValues.includes(value)) {
return true
} else if (falseValues.includes(value)) {
return false
}
throw new Error('Value is not a valid boolean or convertible to a boolean.')
}

View file

@ -5,7 +5,7 @@
</template>
<script>
import { trueString } from '@baserow/modules/database/utils/constants'
import { trueValues } from '@baserow/modules/core/utils/constants'
import viewFilter from '@baserow/modules/database/mixins/viewFilter'
export default {
@ -14,7 +14,7 @@ export default {
computed: {
copy() {
const value = this.filter.value.toString().toLowerCase().trim()
return trueString.includes(value)
return trueValues.includes(value)
},
},
methods: {

View file

@ -122,7 +122,7 @@ import FormViewFieldMultipleLinkRow from '@baserow/modules/database/components/v
import FormViewFieldMultipleSelectCheckboxes from '@baserow/modules/database/components/view/form/FormViewFieldMultipleSelectCheckboxes'
import FormViewFieldSingleSelectRadios from '@baserow/modules/database/components/view/form/FormViewFieldSingleSelectRadios'
import { trueString } from '@baserow/modules/database/utils/constants'
import { trueValues } from '@baserow/modules/core/utils/constants'
import {
getDateMomentFormat,
getFieldTimezone,
@ -1541,7 +1541,7 @@ export class BooleanFieldType extends FieldType {
clipboardData = ''
}
const value = clipboardData.toLowerCase().trim()
return trueString.includes(value)
return trueValues.includes(value)
}
getDocsDataType(field) {

View file

@ -1,4 +1,3 @@
export const trueString = ['y', 't', 'o', 'yes', 'true', 'on', '1']
// Please keep in sync with src/baserow/contrib/database/fields/handler.py:30
export const RESERVED_BASEROW_FIELD_NAMES = ['id', 'order']
export const MAX_FIELD_NAME_LENGTH = 255

View file

@ -10,7 +10,7 @@ import ViewFilterTypeDate from '@baserow/modules/database/components/view/ViewFi
import ViewFilterTypeTimeZone from '@baserow/modules/database/components/view/ViewFilterTypeTimeZone'
import ViewFilterTypeNumberWithTimeZone from '@baserow/modules/database/components/view/ViewFilterTypeNumberWithTimeZone'
import ViewFilterTypeLinkRow from '@baserow/modules/database/components/view/ViewFilterTypeLinkRow'
import { trueString } from '@baserow/modules/database/utils/constants'
import { trueValues } from '@baserow/modules/core/utils/constants'
import {
splitTimezoneAndFilterValue,
DATE_FILTER_TIMEZONE_VALUE_SEPARATOR,
@ -1718,14 +1718,14 @@ export class BooleanViewFilterType extends ViewFilterType {
}
matches(rowValue, filterValue, field, fieldType) {
filterValue = trueString.includes(
filterValue = trueValues.includes(
filterValue.toString().toLowerCase().trim()
)
if (rowValue === null) {
rowValue = false
} else {
rowValue = trueString.includes(rowValue.toString().toLowerCase().trim())
rowValue = trueValues.includes(rowValue.toString().toLowerCase().trim())
}
return filterValue ? rowValue : !rowValue
}