1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-14 17:18:33 +00:00

Merge branch '2517-improve-dropdow-element-to-allow-to-change-the-display' into 'develop'

Resolve "Improve Dropdown element to allow to change the display"

Closes 

See merge request 
This commit is contained in:
Jérémie Pardou 2024-06-13 08:00:02 +00:00
commit 71d9ccae3c
41 changed files with 662 additions and 340 deletions

View file

@ -11,8 +11,8 @@ from baserow.contrib.builder.api.workflow_actions.serializers import (
BuilderWorkflowActionSerializer,
)
from baserow.contrib.builder.elements.models import (
ChoiceElementOption,
CollectionField,
DropdownElementOption,
Element,
)
from baserow.contrib.builder.elements.registries import (
@ -303,7 +303,7 @@ class UpdateCollectionFieldSerializer(serializers.ModelSerializer):
value = FormulaSerializerField(allow_blank=True)
class DropdownOptionSerializer(serializers.ModelSerializer):
class ChoiceOptionSerializer(serializers.ModelSerializer):
class Meta:
model = DropdownElementOption
model = ChoiceElementOption
fields = ["id", "value", "name"]

View file

@ -172,8 +172,8 @@ class BuilderConfig(AppConfig):
from .elements.element_types import (
ButtonElementType,
CheckboxElementType,
ChoiceElementType,
ColumnElementType,
DropdownElementType,
FormContainerElementType,
HeadingElementType,
IFrameElementType,
@ -196,7 +196,7 @@ class BuilderConfig(AppConfig):
element_type_registry.register(TableElementType())
element_type_registry.register(RepeatElementType())
element_type_registry.register(FormContainerElementType())
element_type_registry.register(DropdownElementType())
element_type_registry.register(ChoiceElementType())
element_type_registry.register(CheckboxElementType())
element_type_registry.register(IFrameElementType())

View file

@ -11,7 +11,7 @@ from django.db.models.functions import Cast
from rest_framework import serializers
from rest_framework.exceptions import ValidationError as DRFValidationError
from baserow.contrib.builder.api.elements.serializers import DropdownOptionSerializer
from baserow.contrib.builder.api.elements.serializers import ChoiceOptionSerializer
from baserow.contrib.builder.data_providers.exceptions import (
FormDataProviderChunkInvalidException,
)
@ -26,9 +26,9 @@ from baserow.contrib.builder.elements.models import (
WIDTHS,
ButtonElement,
CheckboxElement,
ChoiceElement,
ChoiceElementOption,
ColumnElement,
DropdownElement,
DropdownElementOption,
Element,
FormContainerElement,
HeadingElement,
@ -1127,10 +1127,17 @@ class CheckboxElementType(InputElementType):
}
class DropdownElementType(FormElementTypeMixin, ElementType):
type = "dropdown"
model_class = DropdownElement
allowed_fields = ["label", "default_value", "required", "placeholder", "multiple"]
class ChoiceElementType(FormElementTypeMixin, ElementType):
type = "choice"
model_class = ChoiceElement
allowed_fields = [
"label",
"default_value",
"required",
"placeholder",
"multiple",
"show_as_dropdown",
]
serializer_field_names = [
"label",
"default_value",
@ -1138,6 +1145,7 @@ class DropdownElementType(FormElementTypeMixin, ElementType):
"placeholder",
"options",
"multiple",
"show_as_dropdown",
]
request_serializer_field_names = [
"label",
@ -1146,6 +1154,7 @@ class DropdownElementType(FormElementTypeMixin, ElementType):
"placeholder",
"options",
"multiple",
"show_as_dropdown",
]
class SerializedDict(ElementDict):
@ -1155,6 +1164,7 @@ class DropdownElementType(FormElementTypeMixin, ElementType):
default_value: BaserowFormula
options: List
multiple: bool
show_as_dropdown: bool
@property
def serializer_field_overrides(self):
@ -1162,36 +1172,41 @@ class DropdownElementType(FormElementTypeMixin, ElementType):
overrides = {
"label": FormulaSerializerField(
help_text=DropdownElement._meta.get_field("label").help_text,
help_text=ChoiceElement._meta.get_field("label").help_text,
required=False,
allow_blank=True,
default="",
),
"default_value": FormulaSerializerField(
help_text=DropdownElement._meta.get_field("default_value").help_text,
help_text=ChoiceElement._meta.get_field("default_value").help_text,
required=False,
allow_blank=True,
default="",
),
"required": serializers.BooleanField(
help_text=DropdownElement._meta.get_field("required").help_text,
help_text=ChoiceElement._meta.get_field("required").help_text,
default=False,
required=False,
),
"placeholder": serializers.CharField(
help_text=DropdownElement._meta.get_field("placeholder").help_text,
help_text=ChoiceElement._meta.get_field("placeholder").help_text,
required=False,
allow_blank=True,
default="",
),
"options": DropdownOptionSerializer(
source="dropdownelementoption_set", many=True, required=False
"options": ChoiceOptionSerializer(
source="choiceelementoption_set", many=True, required=False
),
"multiple": serializers.BooleanField(
help_text=DropdownElement._meta.get_field("multiple").help_text,
help_text=ChoiceElement._meta.get_field("multiple").help_text,
default=False,
required=False,
),
"show_as_dropdown": serializers.BooleanField(
help_text=ChoiceElement._meta.get_field("show_as_dropdown").help_text,
default=True,
required=False,
),
}
return overrides
@ -1200,12 +1215,12 @@ class DropdownElementType(FormElementTypeMixin, ElementType):
def request_serializer_field_overrides(self):
return {
**self.serializer_field_overrides,
"options": DropdownOptionSerializer(many=True, required=False),
"options": ChoiceOptionSerializer(many=True, required=False),
}
def serialize_property(
self,
element: DropdownElement,
element: ChoiceElement,
prop_name: str,
files_zip=None,
storage=None,
@ -1214,7 +1229,7 @@ class DropdownElementType(FormElementTypeMixin, ElementType):
if prop_name == "options":
return [
self.serialize_option(option)
for option in element.dropdownelementoption_set.all()
for option in element.choiceelementoption_set.all()
]
return super().serialize_property(
@ -1260,7 +1275,7 @@ class DropdownElementType(FormElementTypeMixin, ElementType):
cache=None,
**kwargs,
) -> T:
dropdown_element = super().import_serialized(
choice_element = super().import_serialized(
parent,
serialized_values,
id_mapping,
@ -1272,13 +1287,13 @@ class DropdownElementType(FormElementTypeMixin, ElementType):
options = []
for option in serialized_values.get("options", []):
option["dropdown_id"] = dropdown_element.id
option["choice_id"] = choice_element.id
option_deserialized = self.deserialize_option(option)
options.append(option_deserialized)
DropdownElementOption.objects.bulk_create(options)
ChoiceElementOption.objects.bulk_create(options)
return dropdown_element
return choice_element
def create_instance_from_serialized(
self,
@ -1299,15 +1314,15 @@ class DropdownElementType(FormElementTypeMixin, ElementType):
**kwargs,
)
def serialize_option(self, option: DropdownElementOption) -> Dict:
def serialize_option(self, option: ChoiceElementOption) -> Dict:
return {
"value": option.value,
"name": option.name,
"dropdown_id": option.dropdown_id,
"choice_id": option.choice_id,
}
def deserialize_option(self, value: Dict):
return DropdownElementOption(**value)
return ChoiceElementOption(**value)
def get_pytest_params(self, pytest_data_fixture) -> Dict[str, Any]:
return {
@ -1316,39 +1331,37 @@ class DropdownElementType(FormElementTypeMixin, ElementType):
"required": False,
"placeholder": "'some placeholder'",
"multiple": False,
"show_as_dropdown": True,
}
def after_create(self, instance: DropdownElement, values: Dict):
def after_create(self, instance: ChoiceElement, values: Dict):
options = values.get("options", [])
DropdownElementOption.objects.bulk_create(
[DropdownElementOption(dropdown=instance, **option) for option in options]
ChoiceElementOption.objects.bulk_create(
[ChoiceElementOption(choice=instance, **option) for option in options]
)
def after_update(self, instance: DropdownElement, values: Dict):
def after_update(self, instance: ChoiceElement, values: Dict):
options = values.get("options", None)
if options is not None:
DropdownElementOption.objects.filter(dropdown=instance).delete()
DropdownElementOption.objects.bulk_create(
[
DropdownElementOption(dropdown=instance, **option)
for option in options
]
ChoiceElementOption.objects.filter(choice=instance).delete()
ChoiceElementOption.objects.bulk_create(
[ChoiceElementOption(choice=instance, **option) for option in options]
)
def is_valid(self, element: DropdownElement, value: Union[List, str]) -> bool:
def is_valid(self, element: ChoiceElement, value: Union[List, str]) -> bool:
"""
Responsible for validating `DropdownElement` form data. We handle
Responsible for validating `ChoiceElement` form data. We handle
this validation a little differently to ensure that if someone creates
an option with a blank value, it's considered valid.
:param element: The dropdown element.
:param value: The dropdown value we want to validate.
:param element: The choice element.
:param value: The choice value we want to validate.
:return: Whether the value is valid or not for this element.
"""
options = set(element.dropdownelementoption_set.values_list("value", flat=True))
options = set(element.choiceelementoption_set.values_list("value", flat=True))
if element.multiple:
try:

View file

@ -28,6 +28,8 @@ from baserow.core.db import specific_iterator
from baserow.core.exceptions import IdDoesNotExist
from baserow.core.utils import MirrorDict, extract_allowed
old_element_type_map = {"dropdown": "choice"}
class ElementHandler:
allowed_fields_create = [
@ -634,6 +636,11 @@ class ElementHandler:
id_mapping["builder_page_elements"] = {}
element_type = element_type_registry.get(serialized_element["type"])
if element_type in old_element_type_map:
# We met an old element type name. Let's migrate it.
element_type = old_element_type_map[element_type]
created_instance = element_type.import_serialized(
page,
serialized_element,

View file

@ -621,13 +621,14 @@ class InputTextElement(FormElement):
)
class DropdownElement(FormElement):
class ChoiceElement(FormElement):
label = FormulaField(
default="",
help_text="The text label for this dropdown",
help_text="The text label for this choice",
)
default_value = FormulaField(
default="", help_text="This dropdowns input's default value."
default="",
help_text="This choice's input default value.",
)
placeholder = FormulaField(
default="",
@ -635,19 +636,31 @@ class DropdownElement(FormElement):
)
multiple = models.BooleanField(
default=False,
help_text="Whether this dropdown allows users to choose multiple values.",
help_text="Whether this choice allows users to choose multiple values.",
null=True, # TODO zdm remove me in next release
)
show_as_dropdown = models.BooleanField(
default=True,
help_text="Whether to show the choices as a dropdown.",
null=True, # TODO zdm remove me in next release
)
class DropdownElementOption(models.Model):
class ChoiceElementOption(models.Model):
value = models.TextField(
blank=True, default="", help_text="The value of the option"
blank=True,
default="",
help_text="The value of the option",
)
name = models.TextField(
blank=True, default="", help_text="The display name of the option"
blank=True,
default="",
help_text="The display name of the option",
)
choice = models.ForeignKey(
ChoiceElement,
on_delete=models.CASCADE,
)
dropdown = models.ForeignKey(DropdownElement, on_delete=models.CASCADE)
class CheckboxElement(FormElement):

View file

@ -0,0 +1,63 @@
# Generated by Django 4.1.13 on 2024-06-04 08:05
from django.db import migrations, models
import baserow.core.formula.field
def populate_show_as_dropdown_default_value(apps, schema_editor):
"""
Sets the default value for new `show_as_dropdown` property.
"""
ChoiceElement = apps.get_model("builder", "choiceelement")
ChoiceElement.objects.update(show_as_dropdown=True)
class Migration(migrations.Migration):
dependencies = [
("builder", "0021_dropdownelement_multiple"),
]
operations = [
migrations.RenameModel(old_name="DropdownElementOption", new_name="ChoiceElementOption"),
migrations.RenameModel(old_name="DropdownElement", new_name="ChoiceElement"),
migrations.AlterField(
model_name="choiceelement",
name="default_value",
field=baserow.core.formula.field.FormulaField(
default="", help_text="This choice's input default value."
),
),
migrations.AlterField(
model_name="choiceelement",
name="label",
field=baserow.core.formula.field.FormulaField(
default="", help_text="The text label for this choice"
),
),
migrations.AlterField(
model_name="choiceelement",
name="multiple",
field=models.BooleanField(
default=False,
help_text="Whether this choice allows users to choose multiple values.",
null=True,
),
),
migrations.RenameField(
model_name="choiceelementoption",
old_name="dropdown",
new_name="choice",
),
migrations.AddField(
model_name="choiceelement",
name="show_as_dropdown",
field=models.BooleanField(
default=True,
help_text="Whether to show the choices as a dropdown.",
null=True,
),
),
migrations.RunPython(populate_show_as_dropdown_default_value, reverse_code=migrations.RunPython.noop),
]

View file

@ -1,4 +1,4 @@
from typing import Any
from typing import Any, List
from django.core.exceptions import ValidationError
@ -9,12 +9,12 @@ from baserow.contrib.database.fields.constants import (
from baserow.core.utils import flatten
def ensure_boolean(value):
def ensure_boolean(value: Any) -> bool:
"""
Ensures that the value is a boolean or converts it.
@param value: The value to ensure as a boolean.
@returns: The value as a boolean.
:param value: The value to ensure as a boolean.
:return: The value as a boolean.
"""
if value in BASEROW_BOOLEAN_FIELD_TRUE_VALUES:
@ -45,7 +45,7 @@ def ensure_integer(value: Any) -> int:
) from exc
def ensure_string(value, allow_empty=True):
def ensure_string(value: Any, allow_empty: bool = True) -> str:
"""
Ensures that the value is a string or try to convert it.
@ -64,7 +64,7 @@ def ensure_string(value, allow_empty=True):
return str(value)
def ensure_array(value, allow_empty=True):
def ensure_array(value: Any, allow_empty: bool = True) -> List[Any]:
"""
Ensure that the value is an array or try to convert it.
Strings will be treated as comma separated values.
@ -73,7 +73,6 @@ def ensure_array(value, allow_empty=True):
:param value: The value to ensure as an array.
:param allow_empty: Whether we should raise an error if `value` is empty.
:return: The value as an array.
:rtype: list
:raises ValueError: if not allow_empty and `value` is empty.
"""

View file

@ -2,9 +2,9 @@ from copy import deepcopy
from baserow.contrib.builder.elements.models import (
ButtonElement,
ChoiceElement,
CollectionField,
ColumnElement,
DropdownElement,
FormContainerElement,
HeadingElement,
ImageElement,
@ -93,8 +93,8 @@ class ElementFixtures:
)
return element
def create_builder_dropdown_element(self, user=None, page=None, **kwargs):
element = self.create_builder_element(DropdownElement, user, page, **kwargs)
def create_builder_choice_element(self, user=None, page=None, **kwargs):
element = self.create_builder_element(ChoiceElement, user, page, **kwargs)
return element
def create_builder_repeat_element(self, user=None, page=None, **kwargs):

View file

@ -10,7 +10,7 @@ from rest_framework.status import (
)
from baserow.contrib.builder.elements.models import (
DropdownElementOption,
ChoiceElementOption,
Element,
LinkElement,
)
@ -466,7 +466,7 @@ def test_child_type_not_allowed_validation(api_client, data_fixture):
@pytest.mark.django_db
def test_dropdown_options_created(api_client, data_fixture):
def test_choice_options_created(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
page = data_fixture.create_builder_page(user=user)
@ -475,7 +475,7 @@ def test_dropdown_options_created(api_client, data_fixture):
url = reverse("api:builder:element:list", kwargs={"page_id": page.id})
response = api_client.post(
url,
{"type": "dropdown", "options": options},
{"type": "choice", "options": options},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
@ -483,23 +483,21 @@ def test_dropdown_options_created(api_client, data_fixture):
response_json = response.json()
assert response.status_code == HTTP_200_OK
assert response_json["options"][0]["value"] == "'hello'"
assert DropdownElementOption.objects.count() == len(options)
assert ChoiceElementOption.objects.count() == len(options)
@pytest.mark.django_db
def test_dropdown_options_updated(api_client, data_fixture):
def test_choice_options_updated(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
page = data_fixture.create_builder_page(user=user)
dropdown_element = data_fixture.create_builder_dropdown_element(page=page)
choice_element = data_fixture.create_builder_choice_element(page=page)
# Add an existing option
DropdownElementOption.objects.create(dropdown=dropdown_element)
ChoiceElementOption.objects.create(choice=choice_element)
options = [{"value": "'hello'", "name": "'there'"}]
url = reverse(
"api:builder:element:item", kwargs={"element_id": dropdown_element.id}
)
url = reverse("api:builder:element:item", kwargs={"element_id": choice_element.id})
response = api_client.patch(
url,
{"options": options},
@ -510,21 +508,19 @@ def test_dropdown_options_updated(api_client, data_fixture):
response_json = response.json()
assert response.status_code == HTTP_200_OK
assert response_json["options"][0]["value"] == "'hello'"
assert DropdownElementOption.objects.count() == len(options)
assert ChoiceElementOption.objects.count() == len(options)
@pytest.mark.django_db
def test_dropdown_options_deleted(api_client, data_fixture):
def test_choice_options_deleted(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
page = data_fixture.create_builder_page(user=user)
dropdown_element = data_fixture.create_builder_dropdown_element(page=page)
choice_element = data_fixture.create_builder_choice_element(page=page)
# Add an existing option
DropdownElementOption.objects.create(dropdown=dropdown_element)
ChoiceElementOption.objects.create(choice=choice_element)
url = reverse(
"api:builder:element:item", kwargs={"element_id": dropdown_element.id}
)
url = reverse("api:builder:element:item", kwargs={"element_id": choice_element.id})
response = api_client.delete(
url,
format="json",
@ -532,7 +528,7 @@ def test_dropdown_options_deleted(api_client, data_fixture):
)
assert response.status_code == HTTP_204_NO_CONTENT
assert DropdownElementOption.objects.count() == 0
assert ChoiceElementOption.objects.count() == 0
@pytest.mark.django_db

View file

@ -4,8 +4,8 @@ from rest_framework.exceptions import ValidationError
from baserow.contrib.builder.elements.element_types import (
ButtonElementType,
CheckboxElementType,
ChoiceElementType,
ColumnElementType,
DropdownElementType,
HeadingElementType,
IFrameElementType,
ImageElementType,
@ -144,7 +144,7 @@ def test_column_element_type_can_have_children(data_fixture):
place_in_container="1",
)
element_inside_container_nine = ElementHandler().create_element(
DropdownElementType(),
ChoiceElementType(),
page=page,
parent_element_id=container.id,
place_in_container="1",

View file

@ -563,9 +563,7 @@ def test_get_first_ancestor_of_type(data_fixture, django_assert_num_queries):
parent = data_fixture.create_builder_form_container_element(
parent_element=grandparent, page=page
)
child = data_fixture.create_builder_dropdown_element(
page=page, parent_element=parent
)
child = data_fixture.create_builder_choice_element(page=page, parent_element=parent)
with django_assert_num_queries(7):
nearest_column_ancestor = ElementHandler().get_first_ancestor_of_type(

View file

@ -14,7 +14,7 @@ from baserow.contrib.builder.data_providers.exceptions import (
)
from baserow.contrib.builder.elements.element_types import (
CheckboxElementType,
DropdownElementType,
ChoiceElementType,
IFrameElementType,
ImageElementType,
InputTextElementType,
@ -26,8 +26,8 @@ from baserow.contrib.builder.elements.mixins import (
)
from baserow.contrib.builder.elements.models import (
CheckboxElement,
DropdownElement,
DropdownElementOption,
ChoiceElement,
ChoiceElementOption,
HeadingElement,
IFrameElement,
ImageElement,
@ -209,137 +209,137 @@ def test_input_text_element_is_valid(data_fixture):
@pytest.mark.django_db
def test_dropdown_element_import_serialized(data_fixture):
def test_choice_element_import_serialized(data_fixture):
parent = data_fixture.create_builder_page()
dropdown_element = data_fixture.create_builder_dropdown_element(
choice_element = data_fixture.create_builder_choice_element(
page=parent, label="'test'"
)
DropdownElementOption.objects.create(
dropdown=dropdown_element, value="hello", name="there"
ChoiceElementOption.objects.create(
choice=choice_element, value="hello", name="there"
)
serialized_values = DropdownElementType().export_serialized(dropdown_element)
serialized_values = ChoiceElementType().export_serialized(choice_element)
id_mapping = {}
dropdown_element_imported = DropdownElementType().import_serialized(
choice_element_imported = ChoiceElementType().import_serialized(
parent, serialized_values, id_mapping
)
assert dropdown_element.id != dropdown_element_imported.id
assert dropdown_element.label == dropdown_element_imported.label
assert choice_element.id != choice_element_imported.id
assert choice_element.label == choice_element_imported.label
options = dropdown_element_imported.dropdownelementoption_set.all()
options = choice_element_imported.choiceelementoption_set.all()
assert DropdownElementOption.objects.count() == 2
assert ChoiceElementOption.objects.count() == 2
assert len(options) == 1
assert options[0].value == "hello"
assert options[0].name == "there"
assert options[0].dropdown_id == dropdown_element_imported.id
assert options[0].choice_id == choice_element_imported.id
@pytest.mark.django_db
def test_dropdown_element_is_valid(data_fixture):
def test_choice_element_is_valid(data_fixture):
user = data_fixture.create_user()
page = data_fixture.create_builder_page(user=user)
dropdown = ElementService().create_element(
choice = ElementService().create_element(
user=user,
element_type=element_type_registry.get("dropdown"),
element_type=element_type_registry.get("choice"),
page=page,
)
dropdown.dropdownelementoption_set.create(value="uk", name="United Kingdom")
dropdown.dropdownelementoption_set.create(value="it", name="Italy")
choice.choiceelementoption_set.create(value="uk", name="United Kingdom")
choice.choiceelementoption_set.create(value="it", name="Italy")
dropdown.required = True
choice.required = True
with pytest.raises(FormDataProviderChunkInvalidException):
DropdownElementType().is_valid(dropdown, "")
ChoiceElementType().is_valid(choice, "")
assert DropdownElementType().is_valid(dropdown, "uk") == "uk"
assert ChoiceElementType().is_valid(choice, "uk") == "uk"
dropdown.multiple = True
choice.multiple = True
with pytest.raises(FormDataProviderChunkInvalidException):
DropdownElementType().is_valid(dropdown, [])
ChoiceElementType().is_valid(choice, [])
with pytest.raises(FormDataProviderChunkInvalidException):
DropdownElementType().is_valid(dropdown, [""])
ChoiceElementType().is_valid(choice, [""])
assert DropdownElementType().is_valid(dropdown, ["uk"]) == ["uk"]
assert DropdownElementType().is_valid(dropdown, "uk") == ["uk"]
assert DropdownElementType().is_valid(dropdown, ["uk", "it"]) == ["uk", "it"]
assert DropdownElementType().is_valid(dropdown, "uk,it") == ["uk", "it"]
assert ChoiceElementType().is_valid(choice, ["uk"]) == ["uk"]
assert ChoiceElementType().is_valid(choice, "uk") == ["uk"]
assert ChoiceElementType().is_valid(choice, ["uk", "it"]) == ["uk", "it"]
assert ChoiceElementType().is_valid(choice, "uk,it") == ["uk", "it"]
with pytest.raises(FormDataProviderChunkInvalidException):
DropdownElementType().is_valid(dropdown, ["uk", "it", "pt"])
ChoiceElementType().is_valid(choice, ["uk", "it", "pt"])
with pytest.raises(FormDataProviderChunkInvalidException):
DropdownElementType().is_valid(dropdown, "uk,it,pt")
ChoiceElementType().is_valid(choice, "uk,it,pt")
dropdown.multiple = False
dropdown.required = False
choice.multiple = False
choice.required = False
assert DropdownElementType().is_valid(dropdown, "") == ""
assert DropdownElementType().is_valid(dropdown, "uk") == "uk"
assert ChoiceElementType().is_valid(choice, "") == ""
assert ChoiceElementType().is_valid(choice, "uk") == "uk"
dropdown.multiple = True
choice.multiple = True
assert DropdownElementType().is_valid(dropdown, []) == []
assert DropdownElementType().is_valid(dropdown, "") == []
assert ChoiceElementType().is_valid(choice, []) == []
assert ChoiceElementType().is_valid(choice, "") == []
with pytest.raises(FormDataProviderChunkInvalidException):
DropdownElementType().is_valid(dropdown, [""])
ChoiceElementType().is_valid(choice, [""])
assert DropdownElementType().is_valid(dropdown, ["uk"]) == ["uk"]
assert DropdownElementType().is_valid(dropdown, ["uk", "it"]) == ["uk", "it"]
assert DropdownElementType().is_valid(dropdown, "uk,it") == ["uk", "it"]
assert ChoiceElementType().is_valid(choice, ["uk"]) == ["uk"]
assert ChoiceElementType().is_valid(choice, ["uk", "it"]) == ["uk", "it"]
assert ChoiceElementType().is_valid(choice, "uk,it") == ["uk", "it"]
with pytest.raises(FormDataProviderChunkInvalidException):
DropdownElementType().is_valid(dropdown, ["uk", "it", "pt"])
ChoiceElementType().is_valid(choice, ["uk", "it", "pt"])
dropdown.dropdownelementoption_set.create(value="", name="Blank")
dropdown.multiple = False
dropdown.required = True
choice.choiceelementoption_set.create(value="", name="Blank")
choice.multiple = False
choice.required = True
assert DropdownElementType().is_valid(dropdown, "") == ""
assert ChoiceElementType().is_valid(choice, "") == ""
dropdown.multiple = True
choice.multiple = True
with pytest.raises(FormDataProviderChunkInvalidException):
DropdownElementType().is_valid(dropdown, [])
ChoiceElementType().is_valid(choice, [])
with pytest.raises(FormDataProviderChunkInvalidException):
DropdownElementType().is_valid(dropdown, "")
ChoiceElementType().is_valid(choice, "")
assert DropdownElementType().is_valid(dropdown, [""]) == [""]
assert DropdownElementType().is_valid(dropdown, ["uk"]) == ["uk"]
assert DropdownElementType().is_valid(dropdown, "uk") == ["uk"]
assert DropdownElementType().is_valid(dropdown, ["uk", "it"]) == ["uk", "it"]
assert DropdownElementType().is_valid(dropdown, "uk,it") == ["uk", "it"]
assert DropdownElementType().is_valid(dropdown, ["uk", "it", ""]) == [
assert ChoiceElementType().is_valid(choice, [""]) == [""]
assert ChoiceElementType().is_valid(choice, ["uk"]) == ["uk"]
assert ChoiceElementType().is_valid(choice, "uk") == ["uk"]
assert ChoiceElementType().is_valid(choice, ["uk", "it"]) == ["uk", "it"]
assert ChoiceElementType().is_valid(choice, "uk,it") == ["uk", "it"]
assert ChoiceElementType().is_valid(choice, ["uk", "it", ""]) == [
"uk",
"it",
"",
]
with pytest.raises(FormDataProviderChunkInvalidException):
DropdownElementType().is_valid(dropdown, ["uk", "it", "", "pt"])
ChoiceElementType().is_valid(choice, ["uk", "it", "", "pt"])
dropdown.multiple = False
dropdown.required = False
choice.multiple = False
choice.required = False
assert DropdownElementType().is_valid(dropdown, "uk") == "uk"
assert ChoiceElementType().is_valid(choice, "uk") == "uk"
dropdown.multiple = True
choice.multiple = True
assert DropdownElementType().is_valid(dropdown, []) == []
assert DropdownElementType().is_valid(dropdown, [""]) == [""]
assert DropdownElementType().is_valid(dropdown, ["uk"]) == ["uk"]
assert DropdownElementType().is_valid(dropdown, ["uk", "it"]) == ["uk", "it"]
assert DropdownElementType().is_valid(dropdown, ["uk", "it", ""]) == [
assert ChoiceElementType().is_valid(choice, []) == []
assert ChoiceElementType().is_valid(choice, [""]) == [""]
assert ChoiceElementType().is_valid(choice, ["uk"]) == ["uk"]
assert ChoiceElementType().is_valid(choice, ["uk", "it"]) == ["uk", "it"]
assert ChoiceElementType().is_valid(choice, ["uk", "it", ""]) == [
"uk",
"it",
"",
]
with pytest.raises(FormDataProviderChunkInvalidException):
DropdownElementType().is_valid(dropdown, ["uk", "it", "", "pt"])
ChoiceElementType().is_valid(choice, ["uk", "it", "", "pt"])
def test_element_type_import_element_priority():
@ -514,13 +514,13 @@ def test_image_element_import_export(data_fixture, fake, storage):
@pytest.mark.django_db
def test_dropdown_element_import_export(data_fixture):
def test_choice_element_import_export(data_fixture):
page = data_fixture.create_builder_page()
data_source_2 = data_fixture.create_builder_local_baserow_get_row_data_source()
element_type = DropdownElementType()
element_type = ChoiceElementType()
exported_element = data_fixture.create_builder_element(
DropdownElement,
ChoiceElement,
label=f"get('data_source.42.field_1')",
default_value=f"get('data_source.42.field_1')",
placeholder=f"get('data_source.42.field_1')",
@ -546,14 +546,14 @@ def test_dropdown_element_import_export(data_fixture):
@pytest.mark.django_db
def test_dropdown_element_import_old_format(data_fixture):
def test_choice_element_import_old_format(data_fixture):
page = data_fixture.create_builder_page()
element_type = DropdownElementType()
element_type = ChoiceElementType()
serialized = {
"id": 1,
"order": "1.00000000000000000000",
"type": "dropdown",
"type": "dropdown", # Element type is the old one
"parent_element_id": None,
"place_in_container": None,
"visibility": "all",
@ -576,10 +576,17 @@ def test_dropdown_element_import_old_format(data_fixture):
"required": False,
"placeholder": "'test'",
"default_value": "'default'",
"options": [],
"options": [
{"value": "Option 1", "name": "option1"},
{"value": "Option 2", "name": "option2"},
],
# multiple property is missing
# show_as_dropdown property is missing
}
imported_element = element_type.import_serialized(page, serialized, {})
assert isinstance(imported_element.specific, ChoiceElement)
assert imported_element.multiple is False
assert imported_element.show_as_dropdown is True
assert len(imported_element.choiceelementoption_set.all()) == 2

View file

@ -0,0 +1,7 @@
{
"type": "feature",
"message": "[Builder] Rename dropdown element to choice element and allow to display element as dropdown/checkbox/radio",
"issue_number": 2517,
"bullet_points": [],
"created_at": "2024-04-17"
}

View file

@ -1,5 +1,5 @@
<template>
<div @click="toggle">
<div class="ab-checkbox" @click="toggle">
<input
type="checkbox"
:checked="value"

View file

@ -15,7 +15,7 @@
@mousemove="hover(value, disabled)"
>
<div class="ab-dropdownitem__item-name">
<div v-if="multiple">
<div v-if="multiple.value">
<Checkbox :disabled="disabled" :checked="isActive(value)"></Checkbox>
</div>
<slot>
@ -39,7 +39,7 @@
</div>
</a>
<i
v-if="!multiple"
v-if="!multiple.value"
class="ab-dropdownitem__item-active-icon iconoir-check"
></i>
</li>

View file

@ -5,6 +5,7 @@
:class="{
'ab-form-group--horizontal': horizontal,
'ab-form-group--horizontal-variable': horizontalVariable,
'ab-form-group--with-label': label,
}"
>
<label

View file

@ -0,0 +1,78 @@
<template>
<div class="ab-radio" @click="toggle">
<input
type="radio"
:checked="value"
:required="required"
class="ab-radio__input"
:disabled="disabled"
:class="{
'ab-radio--error': error,
'ab-radio--readonly': readOnly,
}"
:aria-disabled="disabled"
/>
<label v-if="hasSlot" class="ab-radio__label">
<slot></slot>
</label>
</div>
</template>
<script>
export default {
name: 'ABRadio',
props: {
/**
* The state of the radio.
*/
value: {
type: Boolean,
required: false,
default: false,
},
/**
* Whether the radio is disabled.
*/
disabled: {
type: Boolean,
required: false,
default: false,
},
/**
* Whether the radio is required.
*/
required: {
type: Boolean,
required: false,
default: false,
},
/**
* Whether the radio is in error state.
*/
error: {
type: Boolean,
required: false,
default: false,
},
/**
* Whether the radio is readonly.
*/
readOnly: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
hasSlot() {
return !!this.$slots.default
},
},
methods: {
toggle() {
if (this.disabled || this.readOnly) return
this.$emit('input', !this.value)
},
},
}
</script>

View file

@ -1,11 +1,10 @@
<template>
<div class="checkbox-element-wrapper">
<div>
<ABCheckbox
v-model="inputValue"
:required="element.required"
:read-only="isEditMode"
:error="displayFormDataError"
class="checkbox-element"
>
{{ resolvedLabel }}
<span
@ -27,11 +26,9 @@ import {
ensureBoolean,
ensureString,
} from '@baserow/modules/core/utils/validator'
import ABCheckbox from '@baserow/modules/builder/components/elements/baseComponents/ABCheckbox'
export default {
name: 'CheckboxElement',
components: { ABCheckbox },
mixins: [formElement],
computed: {
defaultValueResolved() {

View file

@ -0,0 +1,150 @@
<template>
<ABFormGroup
:label="labelResolved"
:required="element.required"
:error-message="displayFormDataError ? $t('error.requiredField') : ''"
>
<ABDropdown
v-if="element.show_as_dropdown"
v-model="inputValue"
class="choice-element"
:class="{
'choice-element--error': displayFormDataError,
}"
:placeholder="
element.options.length
? placeholderResolved
: $t('choiceElement.addOptions')
"
:show-search="false"
:multiple="element.multiple"
@hide="onFormElementTouch"
>
<ABDropdownItem
v-for="option in element.options"
:key="option.id"
:name="option.name || option.value"
:value="option.value"
/>
</ABDropdown>
<template v-else>
<template v-if="element.options.length">
<template v-if="element.multiple">
<ABCheckbox
v-for="option in optionsBooleanResolved"
:key="option.id"
:read-only="isEditMode"
:error="displayFormDataError"
:value="option.booleanValue"
@input="onOptionChange(option, $event)"
>
{{ option.name || option.value }}
</ABCheckbox>
</template>
<template v-else>
<ABRadio
v-for="option in element.options"
:key="option.id"
:read-only="isEditMode"
:error="displayFormDataError"
:value="option.value === inputValue"
@input="onOptionChange(option, $event)"
>
{{ option.name || option.value }}
</ABRadio>
</template>
</template>
<template v-else>{{ $t('choiceElement.addOptions') }}</template>
</template>
</ABFormGroup>
</template>
<script>
import formElement from '@baserow/modules/builder/mixins/formElement'
import {
ensureString,
ensureArray,
} from '@baserow/modules/core/utils/validator'
export default {
name: 'ChoiceElement',
mixins: [formElement],
props: {
/**
* @type {Object}
* @property {string} label - The label displayed above the choice element
* @property {string} default_value - The default value selected
* @property {string} placeholder - The placeholder value of the choice element
* @property {boolean} required - If the element is required for form submission
* @property {boolean} multiple - If the choice element allows multiple selections
* @property {boolean} show_as_dropdown - If the choice element should be displayed as a dropdown
* @property {Array} options - The options of the choice element
*/
element: {
type: Object,
required: true,
},
},
computed: {
labelResolved() {
return ensureString(this.resolveFormula(this.element.label))
},
placeholderResolved() {
return ensureString(this.resolveFormula(this.element.placeholder))
},
defaultValueResolved() {
if (this.element.multiple) {
return ensureArray(
this.resolveFormula(this.element.default_value)
).reduce((acc, value) => {
if (
!acc.includes(value) &&
this.element.options.some((o) => o.value === value)
) {
acc.push(value)
}
return acc
}, [])
} 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 = ensureString(
this.resolveFormula(this.element.default_value)
)
return resolvedSingleValue.length ? resolvedSingleValue : null
}
},
optionsBooleanResolved() {
return this.element.options.map((option) => ({
...option,
booleanValue: this.inputValue.includes(option.value),
}))
},
},
watch: {
defaultValueResolved: {
handler(newValue) {
this.inputValue = newValue
},
immediate: true,
},
'element.multiple'() {
this.setFormData(this.defaultValueResolved)
},
},
methods: {
onOptionChange(option, value) {
if (value) {
if (this.element.multiple) {
this.inputValue = [...this.inputValue, option.value]
} else {
this.inputValue = option.value
}
} else if (this.element.multiple) {
this.inputValue = this.inputValue.filter((v) => v !== option.value)
}
},
},
}
</script>

View file

@ -1,104 +0,0 @@
<template>
<div class="control">
<label v-if="element.label" class="control__label">
{{ labelResolved }}
<span
v-if="element.label && element.required"
:title="$t('error.requiredField')"
>*</span
>
</label>
<ABDropdown
v-model="inputValue"
class="dropdown-element"
:class="{
'dropdown-element--error': displayFormDataError,
}"
:placeholder="placeholderResolved"
:show-search="false"
:multiple="element.multiple"
@hide="onFormElementTouch"
>
<DropdownItem
v-for="option in element.options"
:key="option.id"
:name="option.name || option.value"
:value="option.value"
></DropdownItem>
</ABDropdown>
<div v-if="displayFormDataError" class="error">
<i class="iconoir-warning-triangle"></i>
{{ $t('error.requiredField') }}
</div>
</div>
</template>
<script>
import formElement from '@baserow/modules/builder/mixins/formElement'
import {
ensureString,
ensureArray,
} from '@baserow/modules/core/utils/validator'
export default {
name: 'DropdownElement',
mixins: [formElement],
props: {
/**
* @type {Object}
* @property {string} label - The label displayed above the dropdown
* @property {string} default_value - The default value selected
* @property {string} placeholder - The placeholder value of the dropdown
* @property {boolean} required - If the element is required for form submission
* @property {boolean} multiple - If the dropdown allows multiple selections
* @property {Array} options - The options of the dropdown
*/
element: {
type: Object,
required: true,
},
},
computed: {
labelResolved() {
return ensureString(this.resolveFormula(this.element.label))
},
placeholderResolved() {
return ensureString(this.resolveFormula(this.element.placeholder))
},
defaultValueResolved() {
if (this.element.multiple) {
return ensureArray(
this.resolveFormula(this.element.default_value)
).reduce((acc, value) => {
if (
!acc.includes(value) &&
this.element.options.some((o) => o.value === value)
) {
acc.push(value)
}
return acc
}, [])
} 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 = ensureString(
this.resolveFormula(this.element.default_value)
)
return resolvedSingleValue.length ? resolvedSingleValue : null
}
},
},
watch: {
defaultValueResolved: {
handler(newValue) {
this.inputValue = newValue
},
immediate: true,
},
'element.multiple'() {
this.setFormData(this.defaultValueResolved)
},
},
}
</script>

View file

@ -18,13 +18,39 @@
:placeholder="$t('generalForm.placeholderPlaceholder')"
:data-providers-allowed="DATA_PROVIDERS_ALLOWED_FORM_ELEMENTS"
></ApplicationBuilderFormulaInputGroup>
<FormGroup :label="$t('generalForm.requiredTitle')">
<Checkbox v-model="values.required"></Checkbox>
</FormGroup>
<FormGroup :label="$t('dropdownElementForm.multiple')">
<FormGroup :label="$t('choiceElementForm.multiple')">
<Checkbox v-model="values.multiple"></Checkbox>
</FormGroup>
<DropdownOptionsSelector
<FormGroup :label="$t('choiceElementForm.display')">
<RadioButton
v-model="values.show_as_dropdown"
icon="iconoir-list"
:value="true"
>
{{ $t('choiceElementForm.dropdown') }}
</RadioButton>
<RadioButton
v-model="values.show_as_dropdown"
:icon="
values.multiple ? 'baserow-icon-check-square' : 'iconoir-check-circle'
"
:value="false"
>
{{
values.multiple
? $t('choiceElementForm.checkbox')
: $t('choiceElementForm.radio')
}}
</RadioButton>
</FormGroup>
<ChoiceOptionsSelector
:options="values.options"
@update="optionUpdated"
@create="createOption"
@ -37,12 +63,12 @@
import ApplicationBuilderFormulaInputGroup from '@baserow/modules/builder/components/ApplicationBuilderFormulaInputGroup.vue'
import { DATA_PROVIDERS_ALLOWED_FORM_ELEMENTS } from '@baserow/modules/builder/enums'
import form from '@baserow/modules/core/mixins/form'
import DropdownOptionsSelector from '@baserow/modules/builder/components/elements/components/forms/general/dropdown/DropdownOptionsSelector.vue'
import ChoiceOptionsSelector from '@baserow/modules/builder/components/elements/components/forms/general/choice/ChoiceOptionsSelector.vue'
import { uuid } from '@baserow/modules/core/utils/string'
export default {
name: 'DropdownElementForm',
components: { DropdownOptionsSelector, ApplicationBuilderFormulaInputGroup },
name: 'ChoiceElementForm',
components: { ChoiceOptionsSelector, ApplicationBuilderFormulaInputGroup },
mixins: [form],
inject: ['page'],
data() {
@ -54,6 +80,7 @@ export default {
'placeholder',
'options',
'multiple',
'show_as_dropdown',
],
values: {
label: '',
@ -62,6 +89,7 @@ export default {
placeholder: '',
options: [],
multiple: false,
show_as_dropdown: true,
},
}
},

View file

@ -18,7 +18,7 @@
<script>
export default {
name: 'DropdownOption',
name: 'ChoiceOption',
props: {
option: {
type: Object,

View file

@ -1,14 +1,14 @@
<template>
<FormGroup :label="$t('dropdownOptionSelector.label')" small-label>
<FormGroup :label="$t('choiceOptionSelector.label')" small-label>
<div class="dropdown-option-selector__heading margin-bottom-1">
<div>
{{ $t('dropdownOptionSelector.value') }}
{{ $t('choiceOptionSelector.value') }}
</div>
<div class="dropdown-option-selector__name-heading">
{{ $t('dropdownOptionSelector.name') }}
{{ $t('choiceOptionSelector.name') }}
</div>
</div>
<DropdownOption
<ChoiceOption
v-for="option in options"
:key="option.id"
:option="option"
@ -22,16 +22,16 @@
:loading="loading"
@click="$emit('create')"
>
{{ $t('dropdownOptionSelector.addOption') }}
{{ $t('choiceOptionSelector.addOption') }}
</ButtonText>
</FormGroup>
</template>
<script>
import DropdownOption from '@baserow/modules/builder/components/elements/components/forms/general/dropdown/DropdownOption.vue'
import ChoiceOption from '@baserow/modules/builder/components/elements/components/forms/general/choice/ChoiceOption.vue'
export default {
name: 'DropdownOptionsSelector',
components: { DropdownOption },
name: 'ChoiceOptionsSelector',
components: { ChoiceOption },
props: {
options: {
type: Array,

View file

@ -27,8 +27,8 @@ import RuntimeFormulaContext from '@baserow/modules/core/runtimeFormulaContext'
import { resolveFormula } from '@baserow/modules/core/formula'
import FormContainerElement from '@baserow/modules/builder/components/elements/components/FormContainerElement.vue'
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 ChoiceElement from '@baserow/modules/builder/components/elements/components/ChoiceElement.vue'
import ChoiceElementForm from '@baserow/modules/builder/components/elements/components/forms/general/ChoiceElementForm.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'
import IFrameElement from '@baserow/modules/builder/components/elements/components/IFrameElement.vue'
@ -1210,17 +1210,17 @@ export class ButtonElementType extends ElementType {
}
}
export class DropdownElementType extends FormElementType {
export class ChoiceElementType extends FormElementType {
static getType() {
return 'dropdown'
return 'choice'
}
get name() {
return this.app.i18n.t('elementType.dropdown')
return this.app.i18n.t('elementType.choice')
}
get description() {
return this.app.i18n.t('elementType.dropdownDescription')
return this.app.i18n.t('elementType.choiceDescription')
}
get iconClass() {
@ -1228,11 +1228,11 @@ export class DropdownElementType extends FormElementType {
}
get component() {
return DropdownElement
return ChoiceElement
}
get generalFormComponent() {
return DropdownElementForm
return ChoiceElementForm
}
formDataType(element) {
@ -1262,10 +1262,10 @@ export class DropdownElementType extends FormElementType {
}
/**
* Responsible for validating the dropdown form element. It behaves slightly
* differently so that dropdown options with blank values are valid. We simply
* test if the value is one of the dropdown's own values.
* @param element - The dropdown form element
* Responsible for validating the choice form element. It behaves slightly
* differently so that choice options with blank values are valid. We simply
* test if the value is one of the choice's own values.
* @param element - The choice form element
* @param value - The value we are validating.
* @returns {boolean}
*/
@ -1276,6 +1276,10 @@ export class DropdownElementType extends FormElementType {
return !(element.required && !validOption)
}
isInError({ element, builder }) {
return element.options.length === 0
}
getDataSchema(element) {
const type = this.formDataType(element)
if (type === 'string') {

View file

@ -87,8 +87,8 @@
"tableDescription": "A table element",
"formContainer": "Form",
"formContainerDescription": "A form element",
"dropdown": "Dropdown",
"dropdownDescription": "Dropdown element",
"choice": "Choice",
"choiceDescription": "For single/multiple value selection",
"checkbox": "Checkbox",
"checkboxDescription": "Checkbox element",
"iframe": "IFrame",
@ -408,8 +408,12 @@
"fontSidePanelForm": {
"label": "Font color"
},
"dropdownElementForm": {
"multiple": "Allow multiple values"
"choiceElementForm": {
"multiple": "Allow multiple values",
"display": "Display",
"dropdown": "Dropdown",
"checkbox": "Checkbox",
"radio": "Radio"
},
"tableElementForm": {
"dataSource": "Data source",
@ -512,7 +516,7 @@
"resetToInitialValuesTitle": "Reset to default values after submission",
"resetToInitialValuesDescription": "If checked, the form's default values will be used to reset the form after successful submission. If unchecked, the user's values will remain."
},
"dropdownOptionSelector": {
"choiceOptionSelector": {
"label": "Options",
"value": "Value",
"name": "Name",
@ -550,6 +554,9 @@
"passwordPlaceholder": "Enter your password...",
"selectOrConfigureUserSourceFirst": "Choose a user source to begin using this login element."
},
"choiceElement": {
"addOptions": "Add options to begin using this element..."
},
"userSourceUsersContext": {
"searchPlaceholder": "Search user",
"anonymous": "Anonymous",

View file

@ -36,7 +36,7 @@ import {
ButtonElementType,
TableElementType,
FormContainerElementType,
DropdownElementType,
ChoiceElementType,
CheckboxElementType,
IFrameElementType,
RepeatElementType,
@ -178,7 +178,7 @@ export default (context) => {
app.$registry.register('element', new ColumnElementType(context))
app.$registry.register('element', new FormContainerElementType(context))
app.$registry.register('element', new InputTextElementType(context))
app.$registry.register('element', new DropdownElementType(context))
app.$registry.register('element', new ChoiceElementType(context))
app.$registry.register('element', new CheckboxElementType(context))
app.$registry.register('element', new RepeatElementType(context))

View file

@ -7,6 +7,8 @@ import ABLink from '@baserow/modules/builder/components/elements/baseComponents/
import ABHeading from '@baserow/modules/builder/components/elements/baseComponents/ABHeading'
import ABDropdown from '@baserow/modules/builder/components/elements/baseComponents/ABDropdown'
import ABDropdownItem from '@baserow/modules/builder/components/elements/baseComponents/ABDropdownItem'
import ABCheckbox from '@baserow/modules/builder/components/elements/baseComponents/ABCheckbox.vue'
import ABRadio from '@baserow/modules/builder/components/elements/baseComponents/ABRadio.vue'
function setupVueForAB(Vue) {
Vue.component('ABButton', ABButton)
@ -16,6 +18,8 @@ function setupVueForAB(Vue) {
Vue.component('ABHeading', ABHeading)
Vue.component('ABDropdown', ABDropdown)
Vue.component('ABDropdownItem', ABDropdownItem)
Vue.component('ABCheckbox', ABCheckbox)
Vue.component('ABRadio', ABRadio)
}
setupVueForAB(Vue)

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg width="64px" height="64px" viewBox="0 0 24 24" stroke-width="1.5" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M3 20.4V3.6C3 3.26863 3.26863 3 3.6 3H20.4C20.7314 3 21 3.26863 21 3.6V20.4C21 20.7314 20.7314 21 20.4 21H3.6C3.26863 21 3 20.7314 3 20.4Z" stroke="#000000" stroke-width="1.5"></path><path d="M7 12.5L10 15.5L17 8.5" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>

After

(image error) Size: 493 B

View file

@ -1,3 +1,13 @@
.ab-checkbox {
font-size: 14px;
display: flex;
align-items: center;
&:nth-child(n + 2) {
margin-top: 10px;
}
}
.ab-checkbox__input {
cursor: pointer;
margin: 0;
@ -23,4 +33,5 @@
color: $black;
user-select: none;
padding-bottom: 2px;
margin-left: 10px;
}

View file

@ -1,5 +1,7 @@
.ab-form-group {
margin-bottom: 20px;
&.ab-form-group--with-label {
margin-bottom: 10px;
}
&.ab-form-group--horizontal,
&.ab-form-group--horizontal-variable {

View file

@ -0,0 +1,37 @@
.ab-radio {
font-size: 14px;
display: flex;
align-items: center;
&:nth-child(n + 2) {
margin-top: 10px;
}
}
.ab-radio__input {
cursor: pointer;
margin: 0;
height: 16px;
width: 16px;
border: 1px solid $black;
display: flex;
justify-content: center;
align-items: center;
}
.ab-radio--error {
border-color: $color-error-300;
}
.ab-radio--readonly {
accent-color: $palette-blue-500;
pointer-events: none;
}
.ab-radio__label {
cursor: pointer;
color: $black;
user-select: none;
padding-bottom: 2px;
margin-left: 10px;
}

View file

@ -8,3 +8,4 @@
@import 'ab_link';
@import 'ab_paragraph';
@import 'ab_tag';
@import 'ab_radio';

View file

@ -7,7 +7,6 @@
@import 'button_element';
@import 'table_element';
@import 'baserow_table';
@import 'checkbox_element';
@import 'dropdown_element';
@import 'choice_element';
@import 'iframe_element';
@import 'repeat_element';

View file

@ -1,6 +0,0 @@
.checkbox-element {
font-size: 14px;
display: flex;
align-items: center;
gap: 10px;
}

View file

@ -1,4 +1,4 @@
.dropdown-element {
.choice-element {
border: 1px solid $black;
border-radius: 0;
@ -30,6 +30,6 @@
}
}
.dropdown-element--error {
.choice-element--error {
border-color: $color-error-300;
}

View file

@ -69,11 +69,11 @@ $baserow-icon-prefix: 'baserow-icon-';
// This list must contain all the svg icons in the `modules/core/assets/icons` that must
// automatically become under `baserow-icon-$icon`. All the icons must be designed on a
// 24px by 24px grid.
$baserow-icons: 'circle-empty', 'circle-checked', 'formula', 'hashtag',
'more-horizontal', 'more-vertical', 'reddit', 'single-select', 'twitter',
'lock-open', 'flag', 'heart', 'star', 'thumbs-up', 'history', 'facebook',
'linkedin', 'gitlab', 'file-csv', 'file-pdf', 'file-image', 'file-audio',
'file-video', 'file-code', 'tablet', 'form', 'file-excel', 'kanban',
'file-word', 'file-archive', 'gallery', 'file-powerpoint', 'calendar', 'smile',
'smartphone', 'plus', 'heading-1', 'heading-2', 'heading-3', 'paragraph',
'ordered-list', 'enlarge';
$baserow-icons: 'circle-empty', 'circle-checked', 'check-square', 'formula',
'hashtag', 'more-horizontal', 'more-vertical', 'reddit', 'single-select',
'twitter', 'lock-open', 'flag', 'heart', 'star', 'thumbs-up', 'history',
'facebook', 'linkedin', 'gitlab', 'file-csv', 'file-pdf', 'file-image',
'file-audio', 'file-video', 'file-code', 'tablet', 'form', 'file-excel',
'kanban', 'file-word', 'file-archive', 'gallery', 'file-powerpoint',
'calendar', 'smile', 'smartphone', 'plus', 'heading-1', 'heading-2',
'heading-3', 'paragraph', 'ordered-list', 'enlarge';

View file

@ -16,7 +16,7 @@
@mousemove="hover(value, disabled)"
>
<div class="select__item-name">
<div v-if="multiple">
<div v-if="multiple.value">
<Checkbox :disabled="disabled" :checked="isActive(value)"></Checkbox>
</div>
<slot>
@ -34,7 +34,10 @@
{{ description }}
</div>
</a>
<i v-if="!multiple" class="select__item-active-icon iconoir-check"></i>
<i
v-if="!multiple.value"
class="select__item-active-icon iconoir-check"
></i>
</li>
</template>

View file

@ -14,7 +14,9 @@ export default {
return {
// This is needed to tell all the child components that the dropdown is going
// to be in multiple state.
multiple: this.multiple,
// The reactiveMultiple is an object to deal with the reactivity issue when you
// use provide inject pattern. Don't change it.
multiple: this.reactiveMultiple,
}
},
props: {
@ -99,6 +101,7 @@ export default {
hasDropdownItem: true,
hover: null,
fixedItemsImmutable: this.fixedItems,
reactiveMultiple: { value: this.multiple }, // Used for provide
}
},
computed: {
@ -128,6 +131,9 @@ export default {
this.forceRefreshSelectedValue()
})
},
multiple(newValue) {
this.reactiveMultiple.value = newValue
},
},
mounted() {
// When the component is mounted we want to forcefully reload the selectedName and

View file

@ -74,7 +74,7 @@ export default {
return this.name.match(regex)
},
isActive(value) {
if (this.multiple) {
if (this.multiple.value) {
return this.$parent.value.includes(value)
} else {
return this.$parent.value === value

View file

@ -13,7 +13,7 @@
@click="select(value, disabled)"
@mousemove="hover(value, disabled)"
>
<div v-if="multiple">
<div v-if="multiple.value">
<Checkbox :disabled="disabled" :checked="isActive(value)"></Checkbox>
</div>
<div

View file

@ -1,6 +1,6 @@
import {
CheckboxElementType,
DropdownElementType,
ChoiceElementType,
ElementType,
InputTextElementType,
} from '@baserow/modules/builder/elementTypes'
@ -47,8 +47,8 @@ describe('elementTypes tests', () => {
).toBe(elementType.name)
expect(elementType.getDisplayName({}, {})).toBe(elementType.name)
})
test('DropdownElementType label, default_value & placeholder variations', () => {
const elementType = testApp.getRegistry().get('element', 'dropdown')
test('ChoiceElementType label, default_value & placeholder variations', () => {
const elementType = testApp.getRegistry().get('element', 'choice')
expect(elementType.getDisplayName({ label: "'Animals'" }, {})).toBe(
'Animals'
)
@ -303,16 +303,16 @@ describe('elementTypes tests', () => {
const elementType = new CheckboxElementType()
expect(elementType.isValid({ required: false }, true)).toBe(true)
})
test('DropdownElementType | required | no value.', () => {
const elementType = new DropdownElementType()
test('ChoiceElementType | required | no value.', () => {
const elementType = new ChoiceElementType()
const element = {
required: true,
options: [{ id: 1, value: 'uk', name: 'UK' }],
}
expect(elementType.isValid(element, '')).toBe(false)
})
test('DropdownElementType | required | blank option.', () => {
const elementType = new DropdownElementType()
test('ChoiceElementType | required | blank option.', () => {
const elementType = new ChoiceElementType()
const element = {
required: true,
options: [
@ -322,24 +322,24 @@ describe('elementTypes tests', () => {
}
expect(elementType.isValid(element, '')).toBe(true)
})
test('DropdownElementType | required | valid value.', () => {
const elementType = new DropdownElementType()
test('ChoiceElementType | required | valid value.', () => {
const elementType = new ChoiceElementType()
const element = {
required: true,
options: [{ id: 1, value: 'uk', name: 'UK' }],
}
expect(elementType.isValid(element, 'uk')).toBe(true)
})
test('DropdownElementType | not required | no value.', () => {
const elementType = new DropdownElementType()
test('ChoiceElementType | not required | no value.', () => {
const elementType = new ChoiceElementType()
const element = {
required: false,
options: [{ id: 1, value: 'uk', name: 'UK' }],
}
expect(elementType.isValid(element, '')).toBe(true)
})
test('DropdownElementType | not required | valid value.', () => {
const elementType = new DropdownElementType()
test('ChoiceElementType | not required | valid value.', () => {
const elementType = new ChoiceElementType()
const element = {
required: false,
options: [{ id: 1, value: 'uk', name: 'UK' }],