diff --git a/backend/src/baserow/contrib/builder/apps.py b/backend/src/baserow/contrib/builder/apps.py index 20164f829..309504dfc 100644 --- a/backend/src/baserow/contrib/builder/apps.py +++ b/backend/src/baserow/contrib/builder/apps.py @@ -242,11 +242,13 @@ class BuilderConfig(AppConfig): builder_workflow_action_type_registry.register(LogoutWorkflowActionType()) from .elements.collection_field_types import ( + BooleanCollectionFieldType, LinkCollectionFieldType, TextCollectionFieldType, ) from .elements.registries import collection_field_type_registry + collection_field_type_registry.register(BooleanCollectionFieldType()) collection_field_type_registry.register(TextCollectionFieldType()) collection_field_type_registry.register(LinkCollectionFieldType()) diff --git a/backend/src/baserow/contrib/builder/elements/collection_field_types.py b/backend/src/baserow/contrib/builder/elements/collection_field_types.py index d1e491fc8..c168f6d5e 100644 --- a/backend/src/baserow/contrib/builder/elements/collection_field_types.py +++ b/backend/src/baserow/contrib/builder/elements/collection_field_types.py @@ -6,6 +6,40 @@ from baserow.contrib.builder.formula_importer import import_formula from baserow.core.formula.serializers import FormulaSerializerField +class BooleanCollectionFieldType(CollectionFieldType): + type = "boolean" + allowed_fields = ["value"] + serializer_field_names = ["value"] + + class SerializedDict(TypedDict): + value: bool + + @property + def serializer_field_overrides(self): + return { + "value": FormulaSerializerField( + help_text="The boolean value.", + required=False, + allow_blank=True, + default=False, + ), + } + + def deserialize_property( + self, + prop_name: str, + value: Any, + id_mapping: Dict[str, Any], + data_source_id: Optional[int] = None, + ) -> Any: + if prop_name == "value" and data_source_id: + return import_formula(value, id_mapping, data_source_id=data_source_id) + + return super().deserialize_property( + prop_name, value, id_mapping, data_source_id + ) + + class TextCollectionFieldType(CollectionFieldType): type = "text" allowed_fields = ["value"] diff --git a/backend/tests/baserow/contrib/builder/elements/test_boolean_collection_field_type.py b/backend/tests/baserow/contrib/builder/elements/test_boolean_collection_field_type.py new file mode 100644 index 000000000..f1e36fdf5 --- /dev/null +++ b/backend/tests/baserow/contrib/builder/elements/test_boolean_collection_field_type.py @@ -0,0 +1,132 @@ +""" +Test the BooleanCollectionFieldType class. +""" + +from unittest.mock import patch + +import pytest + +from baserow.contrib.builder.elements.collection_field_types import ( + BooleanCollectionFieldType, +) +from baserow.core.formula.serializers import FormulaSerializerField + +MODULE_PATH = "baserow.contrib.builder.elements.collection_field_types" + + +def test_class_properties_are_set(): + """ + Test that the properties of the class are correctly set. + + Ensure the type, allowed_fields, and serializer_field_names properties + are set to the correct values. + """ + + expected_type = "boolean" + expected_allowed_fields = ["value"] + expected_serializer_field_names = ["value"] + + bool_field_type = BooleanCollectionFieldType() + + assert bool_field_type.type == expected_type + assert bool_field_type.allowed_fields == expected_allowed_fields + assert bool_field_type.serializer_field_names == expected_serializer_field_names + + +def test_serializer_field_overrides_returns_expected_value(): + """ + Ensure the serializer_field_overrides() method returns the expected value. + """ + + result = BooleanCollectionFieldType().serializer_field_overrides + field = result["value"] + + assert type(field) == FormulaSerializerField + assert field.allow_blank is True + assert field.default is False + assert field.required is False + assert field.help_text == "The boolean value." + + +@patch(f"{MODULE_PATH}.CollectionFieldType.deserialize_property") +@patch(f"{MODULE_PATH}.import_formula") +def test_deserialize_property_returns_value_from_import_formula( + mock_import_formula, mock_super_deserialize +): + """ + Ensure the deserialize_property() method uses import_formula() if the + prop_name is 'value' and a data_source_id is provided. + """ + + mock_value = "foo" + mock_import_formula.return_value = mock_value + prop_name = "value" + value = "foo" + id_mapping = {} + data_source_id = 1 + + result = BooleanCollectionFieldType().deserialize_property( + prop_name, + value, + id_mapping, + data_source_id, + ) + + assert result == mock_value + mock_import_formula.assert_called_once_with( + value, + id_mapping, + data_source_id=data_source_id, + ) + mock_super_deserialize.assert_not_called() + + +@patch(f"{MODULE_PATH}.CollectionFieldType.deserialize_property") +@patch(f"{MODULE_PATH}.import_formula") +@pytest.mark.parametrize( + "prop_name,data_source_id", + [ + ("", 1), + (" ", 1), + ("", None), + (" ", None), + # Intentionally misspelt "value" + ("vallue", 1), + ("value", None), + ], +) +def test_deserialize_property_returns_value_from_super_method( + mock_import_formula, + mock_super_deserialize, + prop_name, + data_source_id, +): + """ + Ensure that the value is returned by calling the parent class's + deserialize_property() method. + + If the prop_name is "value" *and* the data_source_id is not empty, the + import_formula() is called. All other combinations should cause the + super method to be called instead. + """ + + mock_value = "foo" + mock_super_deserialize.return_value = mock_value + value = "foo" + id_mapping = {} + + result = BooleanCollectionFieldType().deserialize_property( + prop_name, + value, + id_mapping, + data_source_id, + ) + + assert result == mock_value + mock_import_formula.assert_not_called() + mock_super_deserialize.assert_called_once_with( + prop_name, + value, + id_mapping, + data_source_id, + ) diff --git a/changelog/entries/unreleased/feature/2359_add_a_new_collection_field_type_of_boolean.json b/changelog/entries/unreleased/feature/2359_add_a_new_collection_field_type_of_boolean.json new file mode 100644 index 000000000..aaef89ec6 --- /dev/null +++ b/changelog/entries/unreleased/feature/2359_add_a_new_collection_field_type_of_boolean.json @@ -0,0 +1,7 @@ +{ + "type": "feature", + "message": "Add a new Collection Field Type of Boolean in Application Builder.", + "issue_number": 2359, + "bullet_points": [], + "created_at": "2024-04-04" +} diff --git a/web-frontend/modules/builder/collectionFieldTypes.js b/web-frontend/modules/builder/collectionFieldTypes.js index 6350ba9fa..7e96c8968 100644 --- a/web-frontend/modules/builder/collectionFieldTypes.js +++ b/web-frontend/modules/builder/collectionFieldTypes.js @@ -1,9 +1,14 @@ import { Registerable } from '@baserow/modules/core/registry' +import BooleanField from '@baserow/modules/builder/components/elements/components/collectionField/BooleanField' import TextField from '@baserow/modules/builder/components/elements/components/collectionField/TextField' import LinkField from '@baserow/modules/builder/components/elements/components/collectionField/LinkField' +import BooleanFieldForm from '@baserow/modules/builder/components/elements/components/collectionField/form/BooleanFieldForm' import TextFieldForm from '@baserow/modules/builder/components/elements/components/collectionField/form/TextFieldForm' import LinkFieldForm from '@baserow/modules/builder/components/elements/components/collectionField/form/LinkFieldForm' -import { ensureString } from '@baserow/modules/core/utils/validator' +import { + ensureBoolean, + ensureString, +} from '@baserow/modules/core/utils/validator' import resolveElementUrl from '@baserow/modules/builder/utils/urlResolution' import { pathParametersInError } from '@baserow/modules/builder/utils/params' @@ -38,6 +43,36 @@ export class CollectionFieldType extends Registerable { } } +export class BooleanCollectionFieldType extends CollectionFieldType { + static getType() { + return 'boolean' + } + + get name() { + return this.app.i18n.t('collectionFieldType.boolean') + } + + get component() { + return BooleanField + } + + get formComponent() { + return BooleanFieldForm + } + + getProps(field, { resolveFormula, applicationContext }) { + try { + return { value: ensureBoolean(resolveFormula(field.value)) } + } catch (error) { + return { value: false } + } + } + + getOrder() { + return 5 + } +} + export class TextCollectionFieldType extends CollectionFieldType { static getType() { return 'text' diff --git a/web-frontend/modules/builder/components/elements/baseComponents/ABCheckbox.vue b/web-frontend/modules/builder/components/elements/baseComponents/ABCheckbox.vue index 12726e9fb..c4cca4953 100644 --- a/web-frontend/modules/builder/components/elements/baseComponents/ABCheckbox.vue +++ b/web-frontend/modules/builder/components/elements/baseComponents/ABCheckbox.vue @@ -4,11 +4,13 @@ type="checkbox" :checked="value" :required="required" - :disabled="disabled" class="ab-checkbox__input" + :disabled="disabled" :class="{ 'ab-checkbox--error': error, + 'ab-checkbox--readonly': readOnly, }" + :aria-disabled="disabled" /> <label v-if="hasSlot" class="ab-checkbox__label"> <slot></slot> @@ -52,6 +54,14 @@ export default { required: false, default: false, }, + /** + * Whether the checkbox is readonly. + */ + readOnly: { + type: Boolean, + required: false, + default: false, + }, }, computed: { hasSlot() { @@ -60,7 +70,7 @@ export default { }, methods: { toggle() { - if (this.disabled) return + if (this.disabled || this.readOnly) return this.$emit('input', !this.value) }, }, diff --git a/web-frontend/modules/builder/components/elements/components/CheckboxElement.vue b/web-frontend/modules/builder/components/elements/components/CheckboxElement.vue index d7a11b4fe..d605f9e5a 100644 --- a/web-frontend/modules/builder/components/elements/components/CheckboxElement.vue +++ b/web-frontend/modules/builder/components/elements/components/CheckboxElement.vue @@ -3,7 +3,7 @@ <ABCheckbox v-model="inputValue" :required="element.required" - :disabled="isEditMode" + :read-only="isEditMode" :error="displayFormDataError" class="checkbox-element" > diff --git a/web-frontend/modules/builder/components/elements/components/collectionField/BooleanField.vue b/web-frontend/modules/builder/components/elements/components/collectionField/BooleanField.vue new file mode 100644 index 000000000..b1f9eb732 --- /dev/null +++ b/web-frontend/modules/builder/components/elements/components/collectionField/BooleanField.vue @@ -0,0 +1,19 @@ +<template> + <ABCheckbox :value="value" :read-only="true" /> +</template> + +<script> +import ABCheckbox from '@baserow/modules/builder/components/elements/baseComponents/ABCheckbox' + +export default { + name: 'BooleanField', + components: { ABCheckbox }, + props: { + value: { + type: Boolean, + required: true, + default: false, + }, + }, +} +</script> diff --git a/web-frontend/modules/builder/components/elements/components/collectionField/form/BooleanFieldForm.vue b/web-frontend/modules/builder/components/elements/components/collectionField/form/BooleanFieldForm.vue new file mode 100644 index 000000000..183fa6351 --- /dev/null +++ b/web-frontend/modules/builder/components/elements/components/collectionField/form/BooleanFieldForm.vue @@ -0,0 +1,45 @@ +<template> + <form @submit.prevent @keydown.enter.prevent> + <ApplicationBuilderFormulaInputGroup + v-model="values.value" + :label="$t('textFieldForm.fieldValueLabel')" + :placeholder="$t('textFieldForm.fieldValuePlaceholder')" + :data-providers-allowed="DATA_PROVIDERS_ALLOWED_ELEMENTS" + :application-context-additions="{ + element, + }" + horizontal + /> + </form> +</template> + +<script> +import { DATA_PROVIDERS_ALLOWED_ELEMENTS } from '@baserow/modules/builder/enums' +import form from '@baserow/modules/core/mixins/form' +import ApplicationBuilderFormulaInputGroup from '@baserow/modules/builder/components/ApplicationBuilderFormulaInputGroup' + +export default { + name: 'BooleanFieldForm', + components: { ApplicationBuilderFormulaInputGroup }, + mixins: [form], + props: { + element: { + type: Object, + required: true, + }, + }, + data() { + return { + allowedValues: ['value'], + values: { + value: '', + }, + } + }, + computed: { + DATA_PROVIDERS_ALLOWED_ELEMENTS() { + return DATA_PROVIDERS_ALLOWED_ELEMENTS + }, + }, +} +</script> diff --git a/web-frontend/modules/builder/locales/en.json b/web-frontend/modules/builder/locales/en.json index 570917c4b..c3e31dc48 100644 --- a/web-frontend/modules/builder/locales/en.json +++ b/web-frontend/modules/builder/locales/en.json @@ -430,6 +430,7 @@ "addAction": "add action" }, "collectionFieldType": { + "boolean": "Boolean", "text": "Text", "link": "Link" }, diff --git a/web-frontend/modules/builder/plugin.js b/web-frontend/modules/builder/plugin.js index 5f55c2436..28085e106 100644 --- a/web-frontend/modules/builder/plugin.js +++ b/web-frontend/modules/builder/plugin.js @@ -93,6 +93,7 @@ import { } from '@baserow/modules/builder/workflowActionTypes' import { + BooleanCollectionFieldType, TextCollectionFieldType, LinkCollectionFieldType, } from '@baserow/modules/builder/collectionFieldTypes' @@ -258,6 +259,10 @@ export default (context) => { new UpdateRowWorkflowActionType(context) ) + app.$registry.register( + 'collectionField', + new BooleanCollectionFieldType(context) + ) app.$registry.register( 'collectionField', new TextCollectionFieldType(context) diff --git a/web-frontend/modules/core/assets/scss/components/builder/elements/ab_components/ab_checkbox.scss b/web-frontend/modules/core/assets/scss/components/builder/elements/ab_components/ab_checkbox.scss index e2e4afeea..e61d5925d 100644 --- a/web-frontend/modules/core/assets/scss/components/builder/elements/ab_components/ab_checkbox.scss +++ b/web-frontend/modules/core/assets/scss/components/builder/elements/ab_components/ab_checkbox.scss @@ -13,6 +13,11 @@ border-color: $color-error-300; } +.ab-checkbox--readonly { + accent-color: $palette-blue-500; + pointer-events: none; +} + .ab-checkbox__label { cursor: pointer; color: $black;