1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-07 22:35:36 +00:00

Merge branch '2388-3-add-new-styles' into 'develop'

Improve AB styling v3 - Add a lot of new styles

Closes 

See merge request 
This commit is contained in:
Jérémie Pardou 2024-07-05 15:41:24 +00:00
commit d61ad81a91
77 changed files with 3176 additions and 1072 deletions
backend
changelog/entries/unreleased/feature
web-frontend/modules

View file

@ -11,12 +11,13 @@ from baserow.api.app_auth_providers.serializers import (
)
from baserow.api.polymorphic import PolymorphicSerializer
from baserow.api.services.serializers import PublicServiceSerializer
from baserow.api.user_files.serializers import UserFileSerializer
from baserow.api.user_files.serializers import UserFileField, UserFileSerializer
from baserow.contrib.builder.api.pages.serializers import PathParamSerializer
from baserow.contrib.builder.api.theme.serializers import (
CombinedThemeConfigBlocksSerializer,
serialize_builder_theme,
)
from baserow.contrib.builder.api.validators import image_file_validation
from baserow.contrib.builder.data_sources.models import DataSource
from baserow.contrib.builder.domains.models import Domain
from baserow.contrib.builder.domains.registries import domain_type_registry
@ -98,6 +99,12 @@ class PublicElementSerializer(serializers.ModelSerializer):
def get_type(self, instance):
return element_type_registry.get_by_model(instance.specific_class).type
style_background_file = UserFileField(
allow_null=True,
help_text="The background image file",
validators=[image_file_validation],
)
class Meta:
model = Element
fields = (
@ -111,17 +118,23 @@ class PublicElementSerializer(serializers.ModelSerializer):
"style_border_top_color",
"style_border_top_size",
"style_padding_top",
"style_margin_top",
"style_border_bottom_color",
"style_border_bottom_size",
"style_padding_bottom",
"style_margin_bottom",
"style_border_left_color",
"style_border_left_size",
"style_padding_left",
"style_margin_left",
"style_border_right_color",
"style_border_right_size",
"style_padding_right",
"style_margin_right",
"style_background",
"style_background_color",
"style_background_file",
"style_background_mode",
"style_width",
"role_type",
"roles",

View file

@ -7,6 +7,8 @@ from drf_spectacular.utils import extend_schema_field, extend_schema_serializer
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from baserow.api.user_files.serializers import UserFileField
from baserow.contrib.builder.api.validators import image_file_validation
from baserow.contrib.builder.api.workflow_actions.serializers import (
BuilderWorkflowActionSerializer,
)
@ -42,6 +44,12 @@ class ElementSerializer(serializers.ModelSerializer):
def get_type(self, instance):
return element_type_registry.get_by_model(instance.specific_class).type
style_background_file = UserFileField(
allow_null=True,
help_text="The background image file",
validators=[image_file_validation],
)
class Meta:
model = Element
fields = (
@ -56,17 +64,23 @@ class ElementSerializer(serializers.ModelSerializer):
"style_border_top_color",
"style_border_top_size",
"style_padding_top",
"style_margin_top",
"style_border_bottom_color",
"style_border_bottom_size",
"style_padding_bottom",
"style_margin_bottom",
"style_border_left_color",
"style_border_left_size",
"style_padding_left",
"style_margin_left",
"style_border_right_color",
"style_border_right_size",
"style_padding_right",
"style_margin_right",
"style_background",
"style_background_color",
"style_background_file",
"style_background_mode",
"style_width",
"role_type",
"roles",
@ -102,6 +116,12 @@ class CreateElementSerializer(serializers.ModelSerializer):
"the given id.",
)
style_background_file = UserFileField(
allow_null=True,
help_text="The background image file",
validators=[image_file_validation],
)
class Meta:
model = Element
fields = (
@ -111,28 +131,42 @@ class CreateElementSerializer(serializers.ModelSerializer):
"parent_element_id",
"place_in_container",
"visibility",
"styles",
"style_border_top_color",
"style_border_top_size",
"style_padding_top",
"style_margin_top",
"style_border_bottom_color",
"style_border_bottom_size",
"style_padding_bottom",
"style_margin_bottom",
"style_border_left_color",
"style_border_left_size",
"style_padding_left",
"style_margin_left",
"style_border_right_color",
"style_border_right_size",
"style_padding_right",
"style_margin_right",
"style_background",
"style_background_color",
"style_background_file",
"style_background_mode",
"style_width",
)
extra_kwargs = {
"visibility": {"default": Element.VISIBILITY_TYPES.ALL},
"styles": {"default": dict},
}
class UpdateElementSerializer(serializers.ModelSerializer):
style_background_file = UserFileField(
allow_null=True,
help_text="The background image file",
validators=[image_file_validation],
)
class Meta:
model = Element
fields = (
@ -141,17 +175,23 @@ class UpdateElementSerializer(serializers.ModelSerializer):
"style_border_top_color",
"style_border_top_size",
"style_padding_top",
"style_margin_top",
"style_border_bottom_color",
"style_border_bottom_size",
"style_padding_bottom",
"style_margin_bottom",
"style_border_left_color",
"style_border_left_size",
"style_padding_left",
"style_margin_left",
"style_border_right_color",
"style_border_right_size",
"style_padding_right",
"style_margin_right",
"style_background",
"style_background_color",
"style_background_file",
"style_background_mode",
"style_width",
"role_type",
"roles",

View file

@ -5,6 +5,49 @@ from baserow.contrib.builder.models import Builder
from baserow.contrib.builder.theme.registries import theme_config_block_registry
class DynamicConfigBlockSerializer(serializers.Serializer):
"""
Style overrides for this element.
"""
def __init__(
self,
*args,
property_name=None,
theme_config_block_type_name=None,
serializer_kwargs=None,
**kwargs,
):
if property_name is None:
raise ValueError("Missing property_name parameter")
if theme_config_block_type_name is None:
raise ValueError("Missing theme_block_type parameter")
super().__init__(*args, **kwargs)
if serializer_kwargs is None:
serializer_kwargs = {}
if not isinstance(property_name, list):
property_name = [property_name]
if not isinstance(theme_config_block_type_name, list):
theme_config_block_type_name = [theme_config_block_type_name]
for prop, type_name in zip(property_name, theme_config_block_type_name):
theme_config_block_type = theme_config_block_registry.get(type_name)
self.fields[prop] = theme_config_block_type.get_serializer_class()(
**({"help_text": f"Styles overrides for {prop}"} | serializer_kwargs)
)
# Dynamically create the Meta class with ref name to prevent collision
class DynamicMeta:
type_names = "".join([p.capitalize() for p in theme_config_block_type_name])
ref_name = f"{type_names}ConfigBlockSerializer"
self.Meta = DynamicMeta
def serialize_builder_theme(builder: Builder) -> dict:
"""
A helper function that serializes all theme properties of the provided builder.
@ -35,7 +78,7 @@ def get_combined_theme_config_blocks_serializer_class() -> serializers.Serialize
:return: The generated serializer.
"""
if hasattr(get_combined_theme_config_blocks_serializer_class, "cache"):
if hasattr(get_combined_theme_config_blocks_serializer_class, "cached_class"):
return get_combined_theme_config_blocks_serializer_class.cached_class
if len(theme_config_block_registry.registry.values()) == 0:

View file

@ -56,7 +56,9 @@ class ThemeView(APIView):
ApplicationDoesNotExist: ERROR_APPLICATION_DOES_NOT_EXIST,
}
)
@validate_body(CombinedThemeConfigBlocksSerializer, return_validated=True)
@validate_body(
CombinedThemeConfigBlocksSerializer, return_validated=True, partial=True
)
def patch(self, request, data: Dict, builder_id: int):
builder = BuilderHandler().get_builder(builder_id)

View file

@ -240,6 +240,7 @@ class BuilderConfig(AppConfig):
ColorThemeConfigBlockType,
ImageThemeConfigBlockType,
LinkThemeConfigBlockType,
PageThemeConfigBlockType,
TypographyThemeConfigBlockType,
)
@ -248,6 +249,7 @@ class BuilderConfig(AppConfig):
theme_config_block_registry.register(ButtonThemeConfigBlockType())
theme_config_block_registry.register(LinkThemeConfigBlockType())
theme_config_block_registry.register(ImageThemeConfigBlockType())
theme_config_block_registry.register(PageThemeConfigBlockType())
from .workflow_actions.registries import builder_workflow_action_type_registry
from .workflow_actions.workflow_action_types import (

View file

@ -18,3 +18,9 @@ class VerticalAlignments(models.TextChoices):
class WIDTHS(models.TextChoices):
AUTO = "auto"
FULL = "full"
class BACKGROUND_IMAGE_MODES(models.TextChoices):
TILE = "tile"
FILL = "fill"
FIT = "fit"

View file

@ -161,6 +161,12 @@ class FormContainerElementType(ContainerElementTypeMixin, ElementType):
@property
def serializer_field_overrides(self):
from baserow.contrib.builder.api.theme.serializers import (
DynamicConfigBlockSerializer,
)
from baserow.contrib.builder.theme.theme_config_block_types import (
ButtonThemeConfigBlockType,
)
from baserow.core.formula.serializers import FormulaSerializerField
return {
@ -184,6 +190,12 @@ class FormContainerElementType(ContainerElementTypeMixin, ElementType):
).help_text,
required=False,
),
"styles": DynamicConfigBlockSerializer(
required=False,
property_name="button",
theme_config_block_type_name=ButtonThemeConfigBlockType.type,
serializer_kwargs={"required": False},
),
}
@property
@ -232,6 +244,13 @@ class TableElementType(CollectionElementWithFieldsTypeMixin, ElementType):
@property
def serializer_field_overrides(self):
from baserow.contrib.builder.api.theme.serializers import (
DynamicConfigBlockSerializer,
)
from baserow.contrib.builder.theme.theme_config_block_types import (
ButtonThemeConfigBlockType,
)
return {
**super().serializer_field_overrides,
"orientation": serializers.JSONField(
@ -239,6 +258,12 @@ class TableElementType(CollectionElementWithFieldsTypeMixin, ElementType):
default=get_default_table_orientation,
help_text=TableElement._meta.get_field("orientation").help_text,
),
"styles": DynamicConfigBlockSerializer(
required=False,
property_name="button",
theme_config_block_type_name=ButtonThemeConfigBlockType.type,
serializer_kwargs={"required": False},
),
}
def get_pytest_params(self, pytest_data_fixture) -> Dict[str, Any]:
@ -276,6 +301,25 @@ class RepeatElementType(
orientation: str
items_per_row: dict
@property
def serializer_field_overrides(self):
from baserow.contrib.builder.api.theme.serializers import (
DynamicConfigBlockSerializer,
)
from baserow.contrib.builder.theme.theme_config_block_types import (
ButtonThemeConfigBlockType,
)
return {
**super().serializer_field_overrides,
"styles": DynamicConfigBlockSerializer(
required=False,
property_name="button",
theme_config_block_type_name=ButtonThemeConfigBlockType.type,
serializer_kwargs={"required": False},
),
}
def import_context_addition(self, instance, id_mapping):
return {"data_source_id": instance.data_source_id}
@ -305,6 +349,12 @@ class HeadingElementType(ElementType):
@property
def serializer_field_overrides(self):
from baserow.contrib.builder.api.theme.serializers import (
DynamicConfigBlockSerializer,
)
from baserow.contrib.builder.theme.theme_config_block_types import (
TypographyThemeConfigBlockType,
)
from baserow.core.formula.serializers import FormulaSerializerField
overrides = {
@ -326,6 +376,12 @@ class HeadingElementType(ElementType):
allow_blank=True,
help_text="Heading font color.",
),
"styles": DynamicConfigBlockSerializer(
required=False,
property_name="typography",
theme_config_block_type_name=TypographyThemeConfigBlockType.type,
serializer_kwargs={"required": False},
),
}
return overrides
@ -379,6 +435,12 @@ class TextElementType(ElementType):
@property
def serializer_field_overrides(self):
from baserow.contrib.builder.api.theme.serializers import (
DynamicConfigBlockSerializer,
)
from baserow.contrib.builder.theme.theme_config_block_types import (
TypographyThemeConfigBlockType,
)
from baserow.core.formula.serializers import FormulaSerializerField
return {
@ -393,6 +455,12 @@ class TextElementType(ElementType):
default=TextElement.TEXT_FORMATS.PLAIN,
help_text=TextElement._meta.get_field("format").help_text,
),
"styles": DynamicConfigBlockSerializer(
required=False,
property_name="typography",
theme_config_block_type_name=TypographyThemeConfigBlockType.type,
serializer_kwargs={"required": False},
),
}
def deserialize_property(
@ -636,6 +704,13 @@ class LinkElementType(ElementType):
@property
def serializer_field_overrides(self):
from baserow.contrib.builder.api.theme.serializers import (
DynamicConfigBlockSerializer,
)
from baserow.contrib.builder.theme.theme_config_block_types import (
ButtonThemeConfigBlockType,
LinkThemeConfigBlockType,
)
from baserow.core.formula.serializers import FormulaSerializerField
overrides = (
@ -669,8 +744,18 @@ class LinkElementType(ElementType):
default="primary",
help_text="Button color.",
),
"styles": DynamicConfigBlockSerializer(
required=False,
property_name=["button", "link"],
theme_config_block_type_name=[
ButtonThemeConfigBlockType.type,
LinkThemeConfigBlockType.type,
],
serializer_kwargs={"required": False},
),
}
)
return overrides
def get_pytest_params(self, pytest_data_fixture):
@ -753,6 +838,12 @@ class ImageElementType(ElementType):
@property
def serializer_field_overrides(self):
from baserow.api.user_files.serializers import UserFileSerializer
from baserow.contrib.builder.api.theme.serializers import (
DynamicConfigBlockSerializer,
)
from baserow.contrib.builder.theme.theme_config_block_types import (
ImageThemeConfigBlockType,
)
from baserow.core.formula.serializers import FormulaSerializerField
overrides = {
@ -769,6 +860,12 @@ class ImageElementType(ElementType):
allow_blank=True,
default="",
),
"styles": DynamicConfigBlockSerializer(
required=False,
property_name="image",
theme_config_block_type_name=ImageThemeConfigBlockType.type,
serializer_kwargs={"required": False},
),
}
overrides.update(super().serializer_field_overrides)
@ -777,7 +874,13 @@ class ImageElementType(ElementType):
@property
def request_serializer_field_overrides(self):
from baserow.api.user_files.serializers import UserFileField
from baserow.contrib.builder.api.theme.serializers import (
DynamicConfigBlockSerializer,
)
from baserow.contrib.builder.api.validators import image_file_validation
from baserow.contrib.builder.theme.theme_config_block_types import (
ImageThemeConfigBlockType,
)
overrides = {
"image_file": UserFileField(
@ -798,6 +901,12 @@ class ImageElementType(ElementType):
default=ImageElement._meta.get_field("style_max_width").default,
help_text=ImageElement._meta.get_field("style_max_width").help_text,
),
"styles": DynamicConfigBlockSerializer(
required=False,
property_name="image",
theme_config_block_type_name=ImageThemeConfigBlockType.type,
serializer_kwargs={"required": False},
),
}
if super().request_serializer_field_overrides is not None:
overrides.update(super().request_serializer_field_overrides)
@ -1014,6 +1123,12 @@ class ButtonElementType(ElementType):
@property
def serializer_field_overrides(self):
from baserow.contrib.builder.api.theme.serializers import (
DynamicConfigBlockSerializer,
)
from baserow.contrib.builder.theme.theme_config_block_types import (
ButtonThemeConfigBlockType,
)
from baserow.core.formula.serializers import FormulaSerializerField
overrides = {
@ -1039,6 +1154,12 @@ class ButtonElementType(ElementType):
default="primary",
help_text="Button color.",
),
"styles": DynamicConfigBlockSerializer(
required=False,
property_name="button",
theme_config_block_type_name=ButtonThemeConfigBlockType.type,
serializer_kwargs={"required": False},
),
}
return overrides

View file

@ -35,22 +35,28 @@ class ElementHandler:
allowed_fields_create = [
"parent_element_id",
"place_in_container",
"styles",
"visibility",
"styles",
"style_border_top_color",
"style_border_top_size",
"style_padding_top",
"style_margin_top",
"style_border_bottom_color",
"style_border_bottom_size",
"style_padding_bottom",
"style_margin_bottom",
"style_border_left_color",
"style_border_left_size",
"style_padding_left",
"style_margin_left",
"style_border_right_color",
"style_border_right_size",
"style_padding_right",
"style_margin_right",
"style_background",
"style_background_color",
"style_background_file",
"style_background_mode",
"style_width",
]
@ -62,17 +68,23 @@ class ElementHandler:
"style_border_top_color",
"style_border_top_size",
"style_padding_top",
"style_margin_top",
"style_border_bottom_color",
"style_border_bottom_size",
"style_padding_bottom",
"style_margin_bottom",
"style_border_left_color",
"style_border_left_size",
"style_padding_left",
"style_margin_left",
"style_border_right_color",
"style_border_right_size",
"style_padding_right",
"style_margin_right",
"style_background",
"style_background_color",
"style_background_file",
"style_background_mode",
"style_width",
"role_type",
"roles",

View file

@ -7,6 +7,7 @@ from django.db import models
from django.db.models import SET_NULL, QuerySet
from baserow.contrib.builder.constants import (
BACKGROUND_IMAGE_MODES,
WIDTHS,
HorizontalAlignments,
VerticalAlignments,
@ -29,10 +30,12 @@ if TYPE_CHECKING:
class BackgroundTypes(models.TextChoices):
NONE = "none"
COLOR = "color"
IMAGE = "image"
class WidthTypes(models.TextChoices):
FULL = "full"
FULL_WIDTH = "full-width"
NORMAL = "normal"
MEDIUM = "medium"
SMALL = "small"
@ -155,6 +158,11 @@ class Element(
style_padding_top = models.PositiveIntegerField(
default=10, help_text="Padding size of the top border."
)
style_margin_top = models.PositiveIntegerField(
default=0,
help_text="Margin size of the top border.",
null=True, # TODO zdm remove me after v1.26
)
style_border_bottom_color = models.CharField(
max_length=20,
@ -168,6 +176,11 @@ class Element(
style_padding_bottom = models.PositiveIntegerField(
default=10, help_text="Padding size of the bottom border."
)
style_margin_bottom = models.PositiveIntegerField(
default=0,
help_text="Margin size of the bottom border.",
null=True, # TODO zdm remove me after v1.26
)
style_border_left_color = models.CharField(
max_length=20,
@ -181,6 +194,11 @@ class Element(
style_padding_left = models.PositiveIntegerField(
default=20, help_text="Padding size of the left border."
)
style_margin_left = models.PositiveIntegerField(
default=0,
help_text="Margin size of the left border.",
null=True, # TODO zdm remove me after v1.26
)
style_border_right_color = models.CharField(
max_length=20,
@ -194,6 +212,11 @@ class Element(
style_padding_right = models.PositiveIntegerField(
default=20, help_text="Padding size of the right border."
)
style_margin_right = models.PositiveIntegerField(
default=0,
help_text="Margin size of the right border.",
null=True, # TODO zdm remove me after v1.26
)
style_background = models.CharField(
choices=BackgroundTypes.choices,
@ -208,6 +231,22 @@ class Element(
help_text="The background color if `style_background` is color.",
)
style_background_file = models.ForeignKey(
UserFile,
null=True,
on_delete=models.SET_NULL,
related_name="element_background_image_file",
help_text="An image file uploaded by the user to be used as element background",
)
style_background_mode = models.CharField(
help_text="The mode of the background image",
choices=BACKGROUND_IMAGE_MODES.choices,
max_length=32,
default=BACKGROUND_IMAGE_MODES.FILL,
null=True, # TODO zdm remove me after v1.26
)
style_width = models.CharField(
choices=WidthTypes.choices,
default=WidthTypes.NORMAL,

View file

@ -18,6 +18,7 @@ from baserow.core.registry import (
ModelRegistryMixin,
Registry,
)
from baserow.core.user_files.handler import UserFileHandler
from baserow.core.user_sources.constants import DEFAULT_USER_ROLE_PREFIX
from baserow.core.user_sources.handler import UserSourceHandler
@ -233,6 +234,14 @@ class ElementType(
if prop_name == "order":
return str(element.order)
if prop_name == "style_background_file_id":
return UserFileHandler().export_user_file(
element.style_background_file,
files_zip=files_zip,
storage=storage,
cache=cache,
)
return super().serialize_property(
element, prop_name, files_zip=files_zip, storage=storage, cache=cache
)
@ -268,6 +277,14 @@ class ElementType(
value,
)
if prop_name == "style_background_file_id":
user_file = UserFileHandler().import_user_file(
value, files_zip=files_zip, storage=storage
)
if user_file:
return user_file.id
return None
return value
@abstractmethod

View file

@ -0,0 +1,299 @@
# Generated by Django 4.2.13 on 2024-07-02 16:58
import django.db.models.deletion
from django.db import migrations, models
import baserow.core.fields
def migrate_element_styles(apps, schema_editor):
"""
Migrates on model element styles into the style property.
"""
Element = apps.get_model("builder", "element")
# Set default values for element styles
Element.objects.all().update(
style_margin_left=0,
style_margin_right=0,
style_margin_top=0,
style_margin_bottom=0,
style_background_mode="fill",
)
class Migration(migrations.Migration):
dependencies = [
("core", "0088_remove_blacklistedtoken_user"),
("builder", "0026_add_more_style_properties"),
]
operations = [
migrations.AddField(
model_name="buttonthemeconfigblock",
name="button_border_color",
field=models.CharField(
blank=True,
default="border",
help_text="The border color of buttons",
max_length=20,
),
),
migrations.AddField(
model_name="buttonthemeconfigblock",
name="button_border_radius",
field=models.SmallIntegerField(default=4, help_text="Button border radius"),
),
migrations.AddField(
model_name="buttonthemeconfigblock",
name="button_border_size",
field=models.SmallIntegerField(default=0, help_text="Button border size"),
),
migrations.AddField(
model_name="buttonthemeconfigblock",
name="button_font_family",
field=models.CharField(default="inter", max_length=250),
),
migrations.AddField(
model_name="buttonthemeconfigblock",
name="button_font_size",
field=models.SmallIntegerField(default=13),
),
migrations.AddField(
model_name="buttonthemeconfigblock",
name="button_horizontal_padding",
field=models.SmallIntegerField(
default=12, help_text="Button horizontal padding"
),
),
migrations.AddField(
model_name="buttonthemeconfigblock",
name="button_hover_border_color",
field=models.CharField(
blank=True,
default="border",
help_text="The border color of buttons when hovered",
max_length=20,
),
),
migrations.AddField(
model_name="buttonthemeconfigblock",
name="button_hover_text_color",
field=models.CharField(
blank=True,
default="#ffffffff",
help_text="The text color of buttons when hovered",
max_length=20,
),
),
migrations.AddField(
model_name="buttonthemeconfigblock",
name="button_text_color",
field=models.CharField(
blank=True,
default="#ffffffff",
help_text="The text color of buttons",
max_length=20,
),
),
migrations.AddField(
model_name="buttonthemeconfigblock",
name="button_vertical_padding",
field=models.SmallIntegerField(
default=12, help_text="Button vertical padding"
),
),
migrations.AddField(
model_name="colorthemeconfigblock",
name="main_error_color",
field=models.CharField(default="#FF5A4A", max_length=9),
),
migrations.AddField(
model_name="colorthemeconfigblock",
name="main_success_color",
field=models.CharField(default="#12D452", max_length=9),
),
migrations.AddField(
model_name="colorthemeconfigblock",
name="main_warning_color",
field=models.CharField(default="#FCC74A", max_length=9),
),
migrations.AddField(
model_name="element",
name="style_background_file",
field=models.ForeignKey(
help_text="An image file uploaded by the user to be used as element background",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="element_background_image_file",
to="core.userfile",
),
),
migrations.AddField(
model_name="element",
name="style_background_mode",
field=models.CharField(
choices=[("tile", "Tile"), ("fill", "Fill"), ("fit", "Fit")],
default="fill",
help_text="The mode of the background image",
max_length=32,
null=True,
),
),
migrations.AddField(
model_name="element",
name="style_margin_bottom",
field=models.PositiveIntegerField(
default=0, help_text="Margin size of the bottom border.", null=True
),
),
migrations.AddField(
model_name="element",
name="style_margin_left",
field=models.PositiveIntegerField(
default=0, help_text="Margin size of the left border.", null=True
),
),
migrations.AddField(
model_name="element",
name="style_margin_right",
field=models.PositiveIntegerField(
default=0, help_text="Margin size of the right border.", null=True
),
),
migrations.AddField(
model_name="element",
name="style_margin_top",
field=models.PositiveIntegerField(
default=0, help_text="Margin size of the top border.", null=True
),
),
migrations.AddField(
model_name="linkthemeconfigblock",
name="link_font_family",
field=models.CharField(default="inter", max_length=250),
),
migrations.AddField(
model_name="linkthemeconfigblock",
name="link_font_size",
field=models.SmallIntegerField(default=13),
),
migrations.AddField(
model_name="typographythemeconfigblock",
name="body_font_family",
field=models.CharField(default="inter", max_length=250),
),
migrations.AddField(
model_name="typographythemeconfigblock",
name="heading_1_font_family",
field=models.CharField(default="inter", max_length=250),
),
migrations.AddField(
model_name="typographythemeconfigblock",
name="heading_2_font_family",
field=models.CharField(default="inter", max_length=250),
),
migrations.AddField(
model_name="typographythemeconfigblock",
name="heading_3_font_family",
field=models.CharField(default="inter", max_length=250),
),
migrations.AddField(
model_name="typographythemeconfigblock",
name="heading_4_font_family",
field=models.CharField(default="inter", max_length=250),
),
migrations.AddField(
model_name="typographythemeconfigblock",
name="heading_5_font_family",
field=models.CharField(default="inter", max_length=250),
),
migrations.AddField(
model_name="typographythemeconfigblock",
name="heading_6_font_family",
field=models.CharField(default="inter", max_length=250),
),
migrations.AlterField(
model_name="element",
name="style_background",
field=models.CharField(
choices=[("none", "None"), ("color", "Color"), ("image", "Image")],
default="none",
help_text="What type of background the element should have.",
max_length=20,
),
),
migrations.AlterField(
model_name="element",
name="style_width",
field=models.CharField(
choices=[
("full", "Full"),
("full-width", "Full Width"),
("normal", "Normal"),
("medium", "Medium"),
("small", "Small"),
],
default="normal",
help_text="Indicates the width of the element.",
max_length=20,
),
),
migrations.CreateModel(
name="PageThemeConfigBlock",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"page_background_color",
models.CharField(
blank=True,
default="#ffffffff",
help_text="The background color of the page",
max_length=20,
),
),
(
"page_background_mode",
models.CharField(
choices=[("tile", "Tile"), ("fill", "Fill"), ("fit", "Fit")],
default="tile",
help_text="The mode of the background image",
max_length=32,
),
),
(
"builder",
baserow.core.fields.AutoOneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)s",
to="builder.builder",
),
),
(
"page_background_file",
models.ForeignKey(
help_text="An image file uploaded by the user to be used as page background",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="page_background_image_file",
to="core.userfile",
),
),
],
options={
"abstract": False,
},
),
migrations.RunPython(
migrate_element_styles, reverse_code=migrations.RunPython.noop
),
]

View file

@ -1,8 +1,13 @@
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from baserow.contrib.builder.constants import WIDTHS, HorizontalAlignments
from baserow.contrib.builder.constants import (
BACKGROUND_IMAGE_MODES,
WIDTHS,
HorizontalAlignments,
)
from baserow.core.fields import AutoOneToOneField
from baserow.core.user_files.models import UserFile
class ThemeConfigBlock(models.Model):
@ -33,9 +38,16 @@ class ColorThemeConfigBlock(ThemeConfigBlock):
primary_color = models.CharField(max_length=9, default="#5190efff")
secondary_color = models.CharField(max_length=9, default="#0eaa42ff")
border_color = models.CharField(max_length=9, default="#d7d8d9ff")
main_success_color = models.CharField(max_length=9, default="#12D452")
main_warning_color = models.CharField(max_length=9, default="#FCC74A")
main_error_color = models.CharField(max_length=9, default="#FF5A4A")
class TypographyThemeConfigBlock(ThemeConfigBlock):
body_font_family = models.CharField(
max_length=250,
default="inter",
)
body_font_size = models.SmallIntegerField(default=14)
body_text_color = models.CharField(max_length=9, default="#070810ff")
body_text_alignment = models.CharField(
@ -43,6 +55,10 @@ class TypographyThemeConfigBlock(ThemeConfigBlock):
max_length=10,
default=HorizontalAlignments.LEFT,
)
heading_1_font_family = models.CharField(
max_length=250,
default="inter",
)
heading_1_font_size = models.SmallIntegerField(default=24)
heading_1_text_color = models.CharField(max_length=9, default="#070810ff")
heading_1_text_alignment = models.CharField(
@ -50,6 +66,10 @@ class TypographyThemeConfigBlock(ThemeConfigBlock):
max_length=10,
default=HorizontalAlignments.LEFT,
)
heading_2_font_family = models.CharField(
max_length=250,
default="inter",
)
heading_2_font_size = models.SmallIntegerField(default=20)
heading_2_text_color = models.CharField(max_length=9, default="#070810ff")
heading_2_text_alignment = models.CharField(
@ -57,6 +77,10 @@ class TypographyThemeConfigBlock(ThemeConfigBlock):
max_length=10,
default=HorizontalAlignments.LEFT,
)
heading_3_font_family = models.CharField(
max_length=250,
default="inter",
)
heading_3_font_size = models.SmallIntegerField(default=16)
heading_3_text_color = models.CharField(max_length=9, default="#070810ff")
heading_3_text_alignment = models.CharField(
@ -64,6 +88,10 @@ class TypographyThemeConfigBlock(ThemeConfigBlock):
max_length=10,
default=HorizontalAlignments.LEFT,
)
heading_4_font_family = models.CharField(
max_length=250,
default="inter",
)
heading_4_font_size = models.SmallIntegerField(default=16)
heading_4_text_color = models.CharField(max_length=9, default="#070810ff")
heading_4_text_alignment = models.CharField(
@ -71,6 +99,10 @@ class TypographyThemeConfigBlock(ThemeConfigBlock):
max_length=10,
default=HorizontalAlignments.LEFT,
)
heading_5_font_family = models.CharField(
max_length=250,
default="inter",
)
heading_5_font_size = models.SmallIntegerField(default=14)
heading_5_text_color = models.CharField(max_length=9, default="#070810ff")
heading_5_text_alignment = models.CharField(
@ -78,6 +110,10 @@ class TypographyThemeConfigBlock(ThemeConfigBlock):
max_length=10,
default=HorizontalAlignments.LEFT,
)
heading_6_font_family = models.CharField(
max_length=250,
default="inter",
)
heading_6_font_size = models.SmallIntegerField(default=14)
heading_6_text_color = models.CharField(max_length=9, default="#202128")
heading_6_text_alignment = models.CharField(
@ -88,6 +124,11 @@ class TypographyThemeConfigBlock(ThemeConfigBlock):
class ButtonThemeConfigBlock(ThemeConfigBlock):
button_font_family = models.CharField(
max_length=250,
default="inter",
)
button_font_size = models.SmallIntegerField(default=13)
button_alignment = models.CharField(
choices=HorizontalAlignments.choices,
max_length=10,
@ -109,15 +150,56 @@ class ButtonThemeConfigBlock(ThemeConfigBlock):
blank=True,
help_text="The background color of buttons",
)
button_text_color = models.CharField(
max_length=20,
default="#ffffffff",
blank=True,
help_text="The text color of buttons",
)
button_border_color = models.CharField(
max_length=20,
default="border",
blank=True,
help_text="The border color of buttons",
)
button_border_size = models.SmallIntegerField(
default=0, help_text="Button border size"
)
button_border_radius = models.SmallIntegerField(
default=4, help_text="Button border radius"
)
button_vertical_padding = models.SmallIntegerField(
default=12, help_text="Button vertical padding"
)
button_horizontal_padding = models.SmallIntegerField(
default=12, help_text="Button horizontal padding"
)
button_hover_background_color = models.CharField(
max_length=20,
default="#96baf6ff",
blank=True,
help_text="The background color of buttons when hovered",
)
button_hover_text_color = models.CharField(
max_length=20,
default="#ffffffff",
blank=True,
help_text="The text color of buttons when hovered",
)
button_hover_border_color = models.CharField(
max_length=20,
default="border",
blank=True,
help_text="The border color of buttons when hovered",
)
class LinkThemeConfigBlock(ThemeConfigBlock):
link_font_family = models.CharField(
max_length=250,
default="inter",
)
link_font_size = models.SmallIntegerField(default=13)
link_text_alignment = models.CharField(
choices=HorizontalAlignments.choices,
max_length=10,
@ -173,3 +255,31 @@ class ImageThemeConfigBlock(ThemeConfigBlock):
max_length=32,
default=IMAGE_CONSTRAINT_TYPES.CONTAIN,
)
class PageThemeConfigBlock(ThemeConfigBlock):
"""
Theme for pages.
"""
page_background_color = models.CharField(
max_length=20,
default="#ffffffff",
blank=True,
help_text="The background color of the page",
)
page_background_file = models.ForeignKey(
UserFile,
null=True,
on_delete=models.SET_NULL,
related_name="page_background_image_file",
help_text="An image file uploaded by the user to be used as page background",
)
page_background_mode = models.CharField(
help_text="The mode of the background image",
choices=BACKGROUND_IMAGE_MODES.choices,
max_length=32,
default=BACKGROUND_IMAGE_MODES.TILE,
)

View file

@ -6,19 +6,18 @@ from django.db.models import QuerySet
from baserow.core.registry import (
CustomFieldsInstanceMixin,
CustomFieldsRegistryMixin,
ImportExportMixin,
EasyImportExportMixin,
Instance,
Registry,
)
from baserow.core.utils import extract_allowed
from .models import ThemeConfigBlock
from .types import ThemeConfigBlockSubClass
class ThemeConfigBlockType(
Instance,
ImportExportMixin[ThemeConfigBlock],
EasyImportExportMixin,
CustomFieldsInstanceMixin,
ABC,
):
@ -33,6 +32,31 @@ class ThemeConfigBlockType(
polymorphic.
"""
parent_property_name = "builder"
def get_property_names(self):
"""
We want all properties here to make it easier.
"""
return [
f.name
for f in self.model_class._meta.get_fields()
if f.name not in ["builder", "id"]
]
@property
def allowed_fields(self):
return [
f.name
for f in self.model_class._meta.get_fields()
if f.name not in ["id", "builder"]
]
@property
def serializer_field_names(self):
return self.allowed_fields
@property
def related_name_in_builder_model(self) -> str:
"""
@ -42,15 +66,6 @@ class ThemeConfigBlockType(
return self.model_class.__name__.lower()
def export_serialized(self, instance):
return {field: getattr(instance, field) for field in self.allowed_fields}
def import_serialized(self, parent, serialized_values, id_mapping):
allowed_values = extract_allowed(serialized_values, self.allowed_fields)
theme_config_block = self.model_class(builder=parent, **allowed_values)
theme_config_block.save()
return theme_config_block
def update_properties(
self, builder, **kwargs: dict
) -> Type[ThemeConfigBlockSubClass]:
@ -64,10 +79,14 @@ class ThemeConfigBlockType(
instance = getattr(builder, self.related_name_in_builder_model)
allowed_values = extract_allowed(kwargs, self.allowed_fields)
for key, value in allowed_values.items():
setattr(instance, key, value)
instance.save()
setattr(builder, self.related_name_in_builder_model, instance)
return instance

View file

@ -1,8 +1,12 @@
from baserow.core.user_files.handler import UserFileHandler
from .models import (
ButtonThemeConfigBlock,
ColorThemeConfigBlock,
ImageThemeConfigBlock,
LinkThemeConfigBlock,
PageThemeConfigBlock,
ThemeConfigBlock,
TypographyThemeConfigBlock,
)
from .registries import ThemeConfigBlockType
@ -11,67 +15,11 @@ from .registries import ThemeConfigBlockType
class ColorThemeConfigBlockType(ThemeConfigBlockType):
type = "color"
model_class = ColorThemeConfigBlock
allowed_fields = [
"primary_color",
"secondary_color",
"border_color",
]
serializer_field_names = [
"primary_color",
"secondary_color",
"border_color",
]
class TypographyThemeConfigBlockType(ThemeConfigBlockType):
type = "typography"
model_class = TypographyThemeConfigBlock
allowed_fields = [
"body_font_size",
"body_text_color",
"body_text_alignment",
"heading_1_font_size",
"heading_1_text_color",
"heading_1_text_alignment",
"heading_2_font_size",
"heading_2_text_color",
"heading_2_text_alignment",
"heading_3_font_size",
"heading_3_text_color",
"heading_3_text_alignment",
"heading_4_font_size",
"heading_4_text_color",
"heading_4_text_alignment",
"heading_5_font_size",
"heading_5_text_color",
"heading_5_text_alignment",
"heading_6_font_size",
"heading_6_text_color",
"heading_6_text_alignment",
]
serializer_field_names = [
"body_font_size",
"body_text_color",
"body_text_alignment",
"heading_1_font_size",
"heading_1_text_color",
"heading_1_text_alignment",
"heading_2_font_size",
"heading_2_text_color",
"heading_2_text_alignment",
"heading_3_font_size",
"heading_3_text_color",
"heading_3_text_alignment",
"heading_4_font_size",
"heading_4_text_color",
"heading_4_text_alignment",
"heading_5_font_size",
"heading_5_text_color",
"heading_5_text_alignment",
"heading_6_font_size",
"heading_6_text_color",
"heading_6_text_alignment",
]
def import_serialized(self, parent, serialized_values, id_mapping):
# Translate from old color property names to new names for compat with templates
@ -87,49 +35,91 @@ class TypographyThemeConfigBlockType(ThemeConfigBlockType):
class ButtonThemeConfigBlockType(ThemeConfigBlockType):
type = "button"
model_class = ButtonThemeConfigBlock
allowed_fields = [
"button_background_color",
"button_hover_background_color",
"button_text_alignment",
"button_alignment",
"button_width",
]
serializer_field_names = [
"button_background_color",
"button_hover_background_color",
"button_text_alignment",
"button_alignment",
"button_width",
]
class LinkThemeConfigBlockType(ThemeConfigBlockType):
type = "link"
model_class = LinkThemeConfigBlock
allowed_fields = [
"link_text_alignment",
"link_text_color",
"link_hover_text_color",
]
serializer_field_names = [
"link_text_alignment",
"link_text_color",
"link_hover_text_color",
]
class ImageThemeConfigBlockType(ThemeConfigBlockType):
type = "image"
model_class = ImageThemeConfigBlock
allowed_fields = [
"image_alignment",
"image_max_width",
"image_max_height",
"image_constraint",
]
serializer_field_names = [
"image_alignment",
"image_max_width",
"image_max_height",
"image_constraint",
]
class PageThemeConfigBlockType(ThemeConfigBlockType):
type = "page"
model_class = PageThemeConfigBlock
def get_property_names(self):
"""
Let's replace the page_background_file property with page_background_file_id.
"""
return [
n if n != "page_background_file" else "page_background_file_id"
for n in super().get_property_names()
]
@property
def serializer_field_overrides(self):
from baserow.api.user_files.serializers import UserFileField
from baserow.contrib.builder.api.validators import image_file_validation
return {
"page_background_file": UserFileField(
allow_null=True,
required=False,
help_text="The image file",
validators=[image_file_validation],
),
}
def serialize_property(
self,
theme_config_block: ThemeConfigBlock,
prop_name: str,
files_zip=None,
storage=None,
cache=None,
):
"""
You can customize the behavior of the serialization of a property with this
hook.
"""
if prop_name == "page_background_file_id":
return UserFileHandler().export_user_file(
theme_config_block.page_background_file,
files_zip=files_zip,
storage=storage,
cache=cache,
)
return super().serialize_property(
theme_config_block,
prop_name,
files_zip=files_zip,
storage=storage,
cache=cache,
)
def deserialize_property(
self,
prop_name: str,
value,
id_mapping,
files_zip=None,
storage=None,
cache=None,
**kwargs,
):
if prop_name == "page_background_file_id":
user_file = UserFileHandler().import_user_file(
value, files_zip=files_zip, storage=storage
)
if user_file:
return user_file.id
return None
return value

View file

@ -20,17 +20,23 @@ class ElementDict(TypedDict):
style_border_top_color: str
style_border_top_size: int
style_padding_top: int
style_margin_top: int
style_border_bottom_color: str
style_border_bottom_size: int
style_padding_bottom: int
style_margin_bottom: int
style_border_left_color: str
style_border_left_size: int
style_padding_left: int
style_margin_left: int
style_border_right_color: str
style_border_right_size: int
style_padding_right: int
style_margin_right: int
style_background: str
style_background_color: str
style_background_file_id: str
style_background_mode: str
style_width: str

View file

@ -389,8 +389,9 @@ class EasyImportExportMixin(Generic[T], ABC):
# The parent property name for the model
parent_property_name: str
# The name of the id mapping used for import process
id_mapping_name: str
# The name of the id mapping used for import process. Let it None if you don't need
# this feature.
id_mapping_name: Optional[str] = None
# The model class to create
model_class: Type[T]
@ -417,6 +418,16 @@ class EasyImportExportMixin(Generic[T], ABC):
return getattr(instance, prop_name)
def get_property_names(self):
"""
Returns a list of properties to export/import for this type. By default it uses
the SerializedDict properties.
:returns: a list of property names belonging to instances of this type.
"""
return self.SerializedDict.__annotations__.keys()
def export_serialized(
self,
instance: T,
@ -432,9 +443,7 @@ class EasyImportExportMixin(Generic[T], ABC):
:return: The exported instance as serialized dict.
"""
property_names = self.SerializedDict.__annotations__.keys()
serialized = self.SerializedDict(
serialized = dict(
**{
key: self.serialize_property(
instance,
@ -443,7 +452,7 @@ class EasyImportExportMixin(Generic[T], ABC):
storage=storage,
cache=cache,
)
for key in property_names
for key in self.get_property_names()
}
)
@ -516,11 +525,11 @@ class EasyImportExportMixin(Generic[T], ABC):
:return: The created instance.
"""
if self.id_mapping_name not in id_mapping:
if self.id_mapping_name and self.id_mapping_name not in id_mapping:
id_mapping[self.id_mapping_name] = {}
deserialized_properties = {}
for name in self.SerializedDict.__annotations__.keys():
for name in self.get_property_names():
if name in serialized_values and name != f"{self.parent_property_name}_id":
deserialized_properties[name] = self.deserialize_property(
name,
@ -533,10 +542,11 @@ class EasyImportExportMixin(Generic[T], ABC):
)
# Remove id key
originale_instance_id = deserialized_properties.pop("id")
originale_instance_id = deserialized_properties.pop("id", 0)
# Remove type
deserialized_properties.pop("type")
# Remove type if any
if "type" in deserialized_properties:
deserialized_properties.pop("type")
# Add the parent
deserialized_properties[self.parent_property_name] = parent
@ -550,8 +560,11 @@ class EasyImportExportMixin(Generic[T], ABC):
**kwargs,
)
# Add the created instance to the mapping
id_mapping[self.id_mapping_name][originale_instance_id] = created_instance.id
if self.id_mapping_name:
# Add the created instance to the mapping
id_mapping[self.id_mapping_name][
originale_instance_id
] = created_instance.id
return created_instance

View file

@ -224,6 +224,25 @@ def test_update_element(api_client, data_fixture):
assert response.json()["level"] == 3
@pytest.mark.django_db
def test_update_element_styles(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
page = data_fixture.create_builder_page(user=user)
element1 = data_fixture.create_builder_heading_element(page=page)
url = reverse("api:builder:element:item", kwargs={"element_id": element1.id})
response = api_client.patch(
url,
{"styles": {"typography": {"heading_1_text_color": "#CCCCCCCC"}}},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_200_OK
assert response.json()["styles"] == {
"typography": {"heading_1_text_color": "#CCCCCCCC"}
}
@pytest.mark.django_db
def test_update_element_bad_request(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
@ -256,6 +275,35 @@ def test_update_element_does_not_exist(api_client, data_fixture):
assert response.json()["error"] == "ERROR_ELEMENT_DOES_NOT_EXIST"
@pytest.mark.django_db
def test_update_element_bad_style_property(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
page = data_fixture.create_builder_page(user=user)
element1 = data_fixture.create_builder_heading_element(page=page)
url = reverse("api:builder:element:item", kwargs={"element_id": element1.id})
# Bad root property
response = api_client.patch(
url,
{"styles": {"typpography": {"heading_1_text_color": "#CCCCCCCC"}}},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_200_OK
assert response.json()["styles"] == {}
# Bad theme property
response = api_client.patch(
url,
{"styles": {"typography": {"heading_25_text_color": "#CCCCCCCC"}}},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_200_OK
assert response.json()["styles"] == {"typography": {}}
@pytest.mark.django_db
def test_move_element_empty_payload(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()

View file

@ -151,6 +151,11 @@ def test_get_builder_application(api_client, data_fixture):
)
assert response.status_code == HTTP_200_OK
response_json = response.json()
# Check we have the theme but don't want to check every single property
assert response_json["theme"]["body_text_color"] == "#070810ff"
del response_json["theme"]
assert response_json == {
"favicon_file": UserFileSerializer(application.favicon_file).data,
"id": application.id,
@ -168,44 +173,6 @@ def test_get_builder_application(api_client, data_fixture):
"generative_ai_models_enabled": {},
},
"pages": [],
"theme": {
"body_text_color": "#070810ff",
"body_font_size": 14,
"body_text_alignment": "left",
"primary_color": "#5190efff",
"secondary_color": "#0eaa42ff",
"border_color": "#d7d8d9ff",
"heading_1_font_size": 24,
"heading_1_text_color": "#070810ff",
"heading_1_text_alignment": "left",
"heading_2_font_size": 20,
"heading_2_text_color": "#070810ff",
"heading_2_text_alignment": "left",
"heading_3_font_size": 16,
"heading_3_text_color": "#070810ff",
"heading_3_text_alignment": "left",
"heading_4_font_size": 16,
"heading_4_text_color": "#070810ff",
"heading_4_text_alignment": "left",
"heading_5_font_size": 14,
"heading_5_text_color": "#070810ff",
"heading_5_text_alignment": "left",
"heading_6_font_size": 14,
"heading_6_text_color": "#202128",
"heading_6_text_alignment": "left",
"button_background_color": "primary",
"button_hover_background_color": "#96baf6ff",
"button_alignment": "left",
"button_text_alignment": "center",
"button_width": "auto",
"image_alignment": "left",
"image_constraint": "contain",
"image_max_height": None,
"image_max_width": 100,
"link_text_alignment": "left",
"link_text_color": "primary",
"link_hover_text_color": "#96baf6ff",
},
}
@ -230,6 +197,11 @@ def test_list_builder_applications(api_client, data_fixture):
)
assert response.status_code == HTTP_200_OK
response_json = response.json()
# Check we have the theme but don't want to check every single property
assert response_json[0]["theme"]["body_text_color"] == "#070810ff"
del response_json[0]["theme"]
assert response_json == [
{
"favicon_file": UserFileSerializer(application.favicon_file).data,
@ -248,43 +220,5 @@ def test_list_builder_applications(api_client, data_fixture):
"generative_ai_models_enabled": {},
},
"pages": [],
"theme": {
"body_text_color": "#070810ff",
"body_font_size": 14,
"body_text_alignment": "left",
"primary_color": "#5190efff",
"secondary_color": "#0eaa42ff",
"border_color": "#d7d8d9ff",
"heading_1_font_size": 24,
"heading_1_text_color": "#070810ff",
"heading_1_text_alignment": "left",
"heading_2_font_size": 20,
"heading_2_text_color": "#070810ff",
"heading_2_text_alignment": "left",
"heading_3_font_size": 16,
"heading_3_text_color": "#070810ff",
"heading_3_text_alignment": "left",
"heading_4_font_size": 16,
"heading_4_text_color": "#070810ff",
"heading_4_text_alignment": "left",
"heading_5_font_size": 14,
"heading_5_text_color": "#070810ff",
"heading_5_text_alignment": "left",
"heading_6_font_size": 14,
"heading_6_text_color": "#202128",
"heading_6_text_alignment": "left",
"button_background_color": "primary",
"button_hover_background_color": "#96baf6ff",
"button_alignment": "left",
"button_text_alignment": "center",
"button_width": "auto",
"image_alignment": "left",
"image_constraint": "contain",
"image_max_height": None,
"image_max_width": 100,
"link_text_alignment": "left",
"link_text_color": "primary",
"link_hover_text_color": "#96baf6ff",
},
}
]

View file

@ -207,22 +207,28 @@ def test_builder_application_export(data_fixture):
"place_in_container": None,
"visibility": "all",
"font_color": "default",
"style_background_color": "#ffffffff",
"style_border_bottom_color": "border",
"style_border_bottom_size": 0,
"styles": {},
"style_border_top_color": "border",
"style_border_top_size": 0,
"style_padding_top": 10,
"style_margin_top": 0,
"style_border_bottom_color": "border",
"style_border_bottom_size": 0,
"style_padding_bottom": 10,
"style_margin_bottom": 0,
"style_border_left_color": "border",
"style_border_left_size": 0,
"style_padding_left": 20,
"style_margin_left": 0,
"style_border_right_color": "border",
"style_border_right_size": 0,
"style_width": "normal",
"style_padding_top": 10,
"style_padding_bottom": 10,
"style_padding_left": 20,
"style_padding_right": 20,
"style_margin_right": 0,
"style_background": "none",
"styles": {},
"style_background_color": "#ffffffff",
"style_background_file_id": None,
"style_background_mode": "fill",
"style_width": "normal",
"value": element1.value,
"level": element1.level,
"alignment": "left",
@ -236,22 +242,28 @@ def test_builder_application_export(data_fixture):
"parent_element_id": None,
"place_in_container": None,
"visibility": "all",
"style_background_color": "#ffffffff",
"style_border_bottom_color": "border",
"style_border_bottom_size": 0,
"styles": {},
"style_border_top_color": "border",
"style_border_top_size": 0,
"style_padding_top": 10,
"style_margin_top": 0,
"style_border_bottom_color": "border",
"style_border_bottom_size": 0,
"style_padding_bottom": 10,
"style_margin_bottom": 0,
"style_border_left_color": "border",
"style_border_left_size": 0,
"style_padding_left": 20,
"style_margin_left": 0,
"style_border_right_color": "border",
"style_border_right_size": 0,
"style_width": "normal",
"style_padding_top": 10,
"style_padding_bottom": 10,
"style_padding_left": 20,
"style_padding_right": 20,
"style_margin_right": 0,
"style_background": "none",
"styles": {},
"style_background_color": "#ffffffff",
"style_background_file_id": None,
"style_background_mode": "fill",
"style_width": "normal",
"value": element2.value,
"alignment": "left",
"roles": [],
@ -264,22 +276,28 @@ def test_builder_application_export(data_fixture):
"parent_element_id": None,
"place_in_container": None,
"visibility": "all",
"style_background_color": "#ffffffff",
"style_border_bottom_color": "border",
"style_border_bottom_size": 0,
"styles": {},
"style_border_top_color": "border",
"style_border_top_size": 0,
"style_padding_top": 10,
"style_margin_top": 0,
"style_border_bottom_color": "border",
"style_border_bottom_size": 0,
"style_padding_bottom": 10,
"style_margin_bottom": 0,
"style_border_left_color": "border",
"style_border_left_size": 0,
"style_padding_left": 20,
"style_margin_left": 0,
"style_border_right_color": "border",
"style_border_right_size": 0,
"style_width": "normal",
"style_padding_top": 10,
"style_padding_bottom": 10,
"style_padding_left": 20,
"style_padding_right": 20,
"style_margin_right": 0,
"style_background": "none",
"styles": {},
"style_background_color": "#ffffffff",
"style_background_file_id": None,
"style_background_mode": "fill",
"style_width": "normal",
"order": str(element_container.order),
"column_amount": 3,
"column_gap": 50,
@ -293,22 +311,28 @@ def test_builder_application_export(data_fixture):
"parent_element_id": element_container.id,
"place_in_container": "0",
"visibility": "all",
"style_background_color": "#ffffffff",
"style_border_bottom_color": "border",
"style_border_bottom_size": 0,
"styles": {},
"style_border_top_color": "border",
"style_border_top_size": 0,
"style_padding_top": 10,
"style_margin_top": 0,
"style_border_bottom_color": "border",
"style_border_bottom_size": 0,
"style_padding_bottom": 10,
"style_margin_bottom": 0,
"style_border_left_color": "border",
"style_border_left_size": 0,
"style_padding_left": 20,
"style_margin_left": 0,
"style_border_right_color": "border",
"style_border_right_size": 0,
"style_width": "normal",
"style_padding_top": 10,
"style_padding_bottom": 10,
"style_padding_left": 20,
"style_padding_right": 20,
"style_margin_right": 0,
"style_background": "none",
"styles": {},
"style_background_color": "#ffffffff",
"style_background_file_id": None,
"style_background_mode": "fill",
"style_width": "normal",
"order": str(element_inside_container.order),
"value": element_inside_container.value,
"alignment": "left",
@ -368,22 +392,28 @@ def test_builder_application_export(data_fixture):
"place_in_container": None,
"visibility": "all",
"font_color": "default",
"style_background_color": "#ffffffff",
"style_border_bottom_color": "border",
"style_border_bottom_size": 0,
"styles": {},
"style_border_top_color": "border",
"style_border_top_size": 0,
"style_padding_top": 10,
"style_margin_top": 0,
"style_border_bottom_color": "border",
"style_border_bottom_size": 0,
"style_padding_bottom": 10,
"style_margin_bottom": 0,
"style_border_left_color": "border",
"style_border_left_size": 0,
"style_padding_left": 20,
"style_margin_left": 0,
"style_border_right_color": "border",
"style_border_right_size": 0,
"style_width": "normal",
"style_padding_top": 10,
"style_padding_bottom": 10,
"style_padding_left": 20,
"style_padding_right": 20,
"style_margin_right": 0,
"style_background": "none",
"styles": {},
"style_background_color": "#ffffffff",
"style_background_file_id": None,
"style_background_mode": "fill",
"style_width": "normal",
"value": element3.value,
"level": element3.level,
"alignment": "left",
@ -406,22 +436,28 @@ def test_builder_application_export(data_fixture):
"parent_element_id": None,
"place_in_container": None,
"visibility": "all",
"style_background_color": "#ffffffff",
"style_border_bottom_color": "border",
"style_border_bottom_size": 0,
"styles": {},
"style_border_top_color": "border",
"style_border_top_size": 0,
"style_padding_top": 10,
"style_margin_top": 0,
"style_border_bottom_color": "border",
"style_border_bottom_size": 0,
"style_padding_bottom": 10,
"style_margin_bottom": 0,
"style_border_left_color": "border",
"style_border_left_size": 0,
"style_padding_left": 20,
"style_margin_left": 0,
"style_border_right_color": "border",
"style_border_right_size": 0,
"style_width": "normal",
"style_padding_top": 10,
"style_padding_bottom": 10,
"style_padding_left": 20,
"style_padding_right": 20,
"style_margin_right": 0,
"style_background": "none",
"styles": {},
"style_background_color": "#ffffffff",
"style_background_file_id": None,
"style_background_mode": "fill",
"style_width": "normal",
"items_per_page": 42,
"data_source_id": element4.data_source.id,
"fields": [
@ -470,42 +506,67 @@ def test_builder_application_export(data_fixture):
},
],
"theme": {
"body_text_color": "#070810ff",
"body_font_size": 14,
"body_text_alignment": "left",
"primary_color": "#5190efff",
"secondary_color": "#0eaa42ff",
"border_color": "#d7d8d9ff",
"main_success_color": "#12D452",
"main_error_color": "#FF5A4A",
"main_warning_color": "#FCC74A",
"body_font_family": "inter",
"body_font_size": 14,
"body_text_color": "#070810ff",
"body_text_alignment": "left",
"heading_1_font_family": "inter",
"heading_1_font_size": 24,
"heading_1_text_color": "#070810ff",
"heading_1_text_alignment": "left",
"heading_2_font_family": "inter",
"heading_2_font_size": 20,
"heading_2_text_color": "#070810ff",
"heading_2_text_alignment": "left",
"heading_3_font_family": "inter",
"heading_3_font_size": 16,
"heading_3_text_color": "#070810ff",
"heading_3_text_alignment": "left",
"heading_4_font_family": "inter",
"heading_4_font_size": 16,
"heading_4_text_color": "#070810ff",
"heading_4_text_alignment": "left",
"heading_5_font_family": "inter",
"heading_5_font_size": 14,
"heading_5_text_color": "#070810ff",
"heading_5_text_alignment": "left",
"heading_6_font_family": "inter",
"heading_6_font_size": 14,
"heading_6_text_color": "#202128",
"heading_6_text_alignment": "left",
"button_background_color": "primary",
"button_hover_background_color": "#96baf6ff",
"button_font_family": "inter",
"button_font_size": 13,
"button_alignment": "left",
"button_text_alignment": "center",
"button_width": "auto",
"image_alignment": "left",
"image_constraint": "contain",
"image_max_height": None,
"image_max_width": 100,
"button_background_color": "primary",
"button_text_color": "#ffffffff",
"button_border_color": "border",
"button_border_size": 0,
"button_border_radius": 4,
"button_vertical_padding": 12,
"button_horizontal_padding": 12,
"button_hover_background_color": "#96baf6ff",
"button_hover_text_color": "#ffffffff",
"button_hover_border_color": "border",
"link_font_family": "inter",
"link_font_size": 13,
"link_text_alignment": "left",
"link_text_color": "primary",
"link_hover_text_color": "#96baf6ff",
"image_alignment": "left",
"image_max_width": 100,
"image_max_height": None,
"image_constraint": "contain",
"page_background_color": "#ffffffff",
"page_background_file_id": None,
"page_background_mode": "tile",
},
"id": builder.id,
"name": builder.name,
@ -514,6 +575,433 @@ def test_builder_application_export(data_fixture):
"favicon_file": None,
}
test = {
"pages": [
{
"id": 3,
"name": "search",
"order": 1,
"path": "search",
"path_params": {},
"elements": [
{
"id": 13,
"order": "1.00000000000000000000",
"type": "heading",
"parent_element_id": None,
"place_in_container": None,
"visibility": "all",
"role_type": "allow_all",
"roles": [],
"styles": {},
"style_border_top_color": "border",
"style_border_top_size": 0,
"style_padding_top": 10,
"style_margin_top": 0,
"style_border_bottom_color": "border",
"style_border_bottom_size": 0,
"style_padding_bottom": 10,
"style_margin_bottom": 0,
"style_border_left_color": "border",
"style_border_left_size": 0,
"style_padding_left": 20,
"style_margin_left": 0,
"style_border_right_color": "border",
"style_border_right_size": 0,
"style_padding_right": 20,
"style_margin_right": 0,
"style_background": "none",
"style_background_color": "#ffffffff",
"style_background_file_id": None,
"style_background_mode": "fill",
"style_width": "normal",
"value": "foo",
"font_color": "default",
"level": 2,
"alignment": "left",
},
{
"id": 14,
"order": "2.00000000000000000000",
"type": "text",
"parent_element_id": None,
"place_in_container": None,
"visibility": "all",
"role_type": "allow_all",
"roles": [],
"styles": {},
"style_border_top_color": "border",
"style_border_top_size": 0,
"style_padding_top": 10,
"style_margin_top": 0,
"style_border_bottom_color": "border",
"style_border_bottom_size": 0,
"style_padding_bottom": 10,
"style_margin_bottom": 0,
"style_border_left_color": "border",
"style_border_left_size": 0,
"style_padding_left": 20,
"style_margin_left": 0,
"style_border_right_color": "border",
"style_border_right_size": 0,
"style_padding_right": 20,
"style_margin_right": 0,
"style_background": "none",
"style_background_color": "#ffffffff",
"style_background_file_id": None,
"style_background_mode": "fill",
"style_width": "normal",
"value": "",
"alignment": "left",
"format": "plain",
},
{
"id": 16,
"order": "3.00000000000000000000",
"type": "column",
"parent_element_id": None,
"place_in_container": None,
"visibility": "all",
"role_type": "allow_all",
"roles": [],
"styles": {},
"style_border_top_color": "border",
"style_border_top_size": 0,
"style_padding_top": 10,
"style_margin_top": 0,
"style_border_bottom_color": "border",
"style_border_bottom_size": 0,
"style_padding_bottom": 10,
"style_margin_bottom": 0,
"style_border_left_color": "border",
"style_border_left_size": 0,
"style_padding_left": 20,
"style_margin_left": 0,
"style_border_right_color": "border",
"style_border_right_size": 0,
"style_padding_right": 20,
"style_margin_right": 0,
"style_background": "none",
"style_background_color": "#ffffffff",
"style_background_file_id": None,
"style_background_mode": "fill",
"style_width": "normal",
"column_amount": 3,
"column_gap": 50,
"alignment": "top",
},
{
"id": 17,
"order": "4.00000000000000000000",
"type": "text",
"parent_element_id": 16,
"place_in_container": "0",
"visibility": "all",
"role_type": "allow_all",
"roles": [],
"styles": {},
"style_border_top_color": "border",
"style_border_top_size": 0,
"style_padding_top": 10,
"style_margin_top": 0,
"style_border_bottom_color": "border",
"style_border_bottom_size": 0,
"style_padding_bottom": 10,
"style_margin_bottom": 0,
"style_border_left_color": "border",
"style_border_left_size": 0,
"style_padding_left": 20,
"style_margin_left": 0,
"style_border_right_color": "border",
"style_border_right_size": 0,
"style_padding_right": 20,
"style_margin_right": 0,
"style_background": "none",
"style_background_color": "#ffffffff",
"style_background_file_id": None,
"style_background_mode": "fill",
"style_width": "normal",
"value": "",
"alignment": "left",
"format": "plain",
},
],
"data_sources": [
{
"id": 1,
"name": "source 1",
"order": "1.00000000000000000000",
"service": {
"id": 1,
"integration_id": 2,
"type": "local_baserow_get_row",
"table_id": None,
"view_id": None,
"filter_type": "AND",
"filters": [],
"row_id": "",
"search_query": "",
},
}
],
"workflow_actions": [
{
"id": 1,
"type": "notification",
"order": 0,
"page_id": 3,
"element_id": 13,
"event": "click",
"title": "there",
"description": "hello",
}
],
},
{
"id": 4,
"name": "index",
"order": 2,
"path": "index",
"path_params": {},
"elements": [
{
"id": 15,
"order": "1.00000000000000000000",
"type": "heading",
"parent_element_id": None,
"place_in_container": None,
"visibility": "all",
"role_type": "allow_all",
"roles": [],
"styles": {},
"style_border_top_color": "border",
"style_border_top_size": 0,
"style_padding_top": 10,
"style_margin_top": 0,
"style_border_bottom_color": "border",
"style_border_bottom_size": 0,
"style_padding_bottom": 10,
"style_margin_bottom": 0,
"style_border_left_color": "border",
"style_border_left_size": 0,
"style_padding_left": 20,
"style_margin_left": 0,
"style_border_right_color": "border",
"style_border_right_size": 0,
"style_padding_right": 20,
"style_margin_right": 0,
"style_background": "none",
"style_background_color": "#ffffffff",
"style_background_file_id": None,
"style_background_mode": "fill",
"style_width": "normal",
"value": "",
"font_color": "default",
"level": 1,
"alignment": "left",
},
{
"id": 18,
"order": "2.00000000000000000000",
"type": "table",
"parent_element_id": None,
"place_in_container": None,
"visibility": "all",
"role_type": "allow_all",
"roles": [],
"styles": {},
"style_border_top_color": "border",
"style_border_top_size": 0,
"style_padding_top": 10,
"style_margin_top": 0,
"style_border_bottom_color": "border",
"style_border_bottom_size": 0,
"style_padding_bottom": 10,
"style_margin_bottom": 0,
"style_border_left_color": "border",
"style_border_left_size": 0,
"style_padding_left": 20,
"style_margin_left": 0,
"style_border_right_color": "border",
"style_border_right_size": 0,
"style_padding_right": 20,
"style_margin_right": 0,
"style_background": "none",
"style_background_color": "#ffffffff",
"style_background_file_id": None,
"style_background_mode": "fill",
"style_width": "normal",
"data_source_id": 3,
"items_per_page": 42,
"button_load_more_label": "",
"fields": [
{
"uid": "447cbec7-c422-42eb-bd50-204b53453330",
"name": "Field 1",
"type": "text",
"config": {"value": "get('test1')"},
},
{
"uid": "44446a1c-841f-47ba-b1df-e902cc50c6ed",
"name": "Field 2",
"type": "text",
"config": {"value": "get('test2')"},
},
{
"uid": "960aef1f-a894-4003-8cf2-36da3b9c798b",
"name": "Field 3",
"type": "text",
"config": {"value": "get('test3')"},
},
],
"button_color": "primary",
"orientation": {
"tablet": "horizontal",
"desktop": "horizontal",
"smartphone": "horizontal",
},
},
],
"data_sources": [
{
"id": 2,
"name": "source 2",
"order": "1.00000000000000000000",
"service": {
"id": 2,
"integration_id": 2,
"type": "local_baserow_get_row",
"table_id": None,
"view_id": None,
"filter_type": "AND",
"filters": [],
"row_id": "",
"search_query": "",
},
},
{
"id": 3,
"name": "source 3",
"order": "2.00000000000000000000",
"service": {
"id": 3,
"integration_id": 2,
"type": "local_baserow_list_rows",
"table_id": None,
"view_id": None,
"search_query": "",
"filter_type": "AND",
"filters": [],
"sortings": [],
},
},
],
"workflow_actions": [],
},
],
"integrations": [
{
"id": 2,
"name": "test",
"order": "1.00000000000000000000",
"type": "local_baserow",
"authorized_user": "jennifer92@example.net",
}
],
"theme": {
"id": 1,
"primary_color": "#5190efff",
"secondary_color": "#0eaa42ff",
"border_color": "#d7d8d9ff",
"main_success_color": "#12D452",
"main_error_color": "#FF5A4A",
"main_warning_color": "#FCC74A",
"body_font_family": "inter",
"body_font_size": 14,
"body_text_color": "#070810ff",
"body_text_alignment": "left",
"heading_1_font_family": "inter",
"heading_1_font_size": 24,
"heading_1_text_color": "#070810ff",
"heading_1_text_alignment": "left",
"heading_2_font_family": "inter",
"heading_2_font_size": 20,
"heading_2_text_color": "#070810ff",
"heading_2_text_alignment": "left",
"heading_3_font_family": "inter",
"heading_3_font_size": 16,
"heading_3_text_color": "#070810ff",
"heading_3_text_alignment": "left",
"heading_4_font_family": "inter",
"heading_4_font_size": 16,
"heading_4_text_color": "#070810ff",
"heading_4_text_alignment": "left",
"heading_5_font_family": "inter",
"heading_5_font_size": 14,
"heading_5_text_color": "#070810ff",
"heading_5_text_alignment": "left",
"heading_6_font_family": "inter",
"heading_6_font_size": 14,
"heading_6_text_color": "#202128",
"heading_6_text_alignment": "left",
"button_font_family": "inter",
"button_font_size": 13,
"button_alignment": "left",
"button_text_alignment": "center",
"button_width": "auto",
"button_background_color": "primary",
"button_text_color": "#ffffffff",
"button_border_color": "border",
"button_border_size": 0,
"button_border_radius": 4,
"button_vertical_padding": 12,
"button_horizontal_padding": 12,
"button_hover_background_color": "#96baf6ff",
"button_hover_text_color": "#ffffffff",
"button_hover_border_color": "border",
"link_font_family": "inter",
"link_font_size": 13,
"link_text_alignment": "left",
"link_text_color": "primary",
"link_hover_text_color": "#96baf6ff",
"image_alignment": "left",
"image_max_width": 100,
"image_max_height": None,
"image_constraint": "contain",
"page_background_color": "#ffffffff",
"page_background_file_id": None,
"page_background_mode": "tile",
},
"user_sources": [
{
"id": 1,
"name": "",
"order": "1.00000000000000000000",
"type": "local_baserow",
"uid": "12345678123456781234567812345678",
"integration_id": 2,
"auth_providers": [
{
"id": 1,
"type": "local_baserow_password",
"domain": None,
"enabled": True,
"password_field_id": None,
}
],
"table_id": None,
"email_field_id": None,
"name_field_id": None,
"role_field_id": None,
}
],
"favicon_file": None,
"id": 5,
"name": "Monica Baldwin",
"order": 0,
"type": "builder",
}
assert serialized == reference

View file

@ -0,0 +1,7 @@
{
"type": "feature",
"message": "[Builder] More styles of the builder elements can be customized",
"issue_number": 2388,
"bullet_points": [],
"created_at": "2024-07-04"
}

View file

@ -0,0 +1,30 @@
<template>
<Dropdown :value="value" fixed-items small @input="$emit('input', $event)">
<DropdownItem
v-for="fontFamily in fontFamilies"
:key="fontFamily.getType()"
:value="fontFamily.getType()"
:name="fontFamily.name"
/>
</Dropdown>
</template>
<script>
export default {
name: 'FontFamilySelector',
props: {
value: {
type: String,
required: false,
default: 'Inter',
},
},
computed: {
fontFamilies() {
return Object.values(this.$registry.getAll('fontFamily')).sort((a, b) =>
a.name.localeCompare(b.name)
)
},
},
}
</script>

View file

@ -0,0 +1,40 @@
<template>
<div class="padding-selector">
<FormInput
small
:value="value.horizontal"
type="number"
remove-number-input-controls
:to-value="(val) => parseInt(val)"
class="padding-selector__input"
icon-right="iconoir-horizontal-split"
@input="$emit('input', { horizontal: $event, vertical: value.vertical })"
@blur="$emit('blur')"
/>
<FormInput
small
:value="value.vertical"
type="number"
remove-number-input-controls
:to-value="(val) => parseInt(val)"
class="padding-selector__input"
icon-right="iconoir-vertical-split"
@input="
$emit('input', { horizontal: value.horizontal, vertical: $event })
"
@blur="$emit('blur')"
/>
</div>
</template>
<script>
export default {
name: 'PaddingSelector',
props: {
value: {
type: Object,
required: true,
},
},
}
</script>

View file

@ -0,0 +1,29 @@
<template>
<FormInput
small
:value="value"
type="number"
remove-number-input-controls
:to-value="(val) => parseInt(val)"
:style="{
width: '100px',
}"
@input="$emit('input', $event)"
@blur="$emit('blur')"
>
<template #suffix>px</template>
</FormInput>
</template>
<script>
export default {
name: 'PixelValueSelector',
props: {
value: {
type: Number,
required: false,
default: null,
},
},
}
</script>

View file

@ -9,7 +9,7 @@
}"
@click="onClick"
>
<slot></slot>
<span><slot></slot></span>
</button>
</template>

View file

@ -1,12 +1,5 @@
<template>
<form class="table-element-form" @submit.prevent @keydown.enter.prevent>
<CustomStyle
v-model="values.styles"
style-key="button"
:config-block-types="['button']"
:theme="builder.theme"
:element="values"
/>
<FormGroup
class="margin-bottom-2"
small-label
@ -53,7 +46,6 @@
:config-block-types="['button']"
:theme="builder.theme"
/>
<ApplicationBuilderFormulaInputGroup
v-model="values.button_load_more_label"
:label="$t('tableElementForm.buttonLoadMoreLabel')"

View file

@ -19,6 +19,7 @@
style-key="typography"
:config-block-types="['typography']"
:theme="builder.theme"
:extra-args="{ onlyBody: values.format === TEXT_FORMAT_TYPES.PLAIN }"
/>
<ApplicationBuilderFormulaInputGroup
v-model="values.value"

View file

@ -7,6 +7,7 @@
/>
<Context ref="context">
<div v-auto-overflow-scroll class="custom-style__config-blocks">
<h2>{{ $t('customStyle.themeOverrides') }}</h2>
<div
v-for="(themeConfigBlock, index) in themeConfigBlocks"
:key="themeConfigBlock.getType()"
@ -20,7 +21,7 @@
</h2>
<ThemeConfigBlock
:theme="theme"
:default-values="value[styleKey] || {}"
:default-values="value?.[styleKey]"
:preview="false"
:theme-config-block-type="themeConfigBlock"
:class="{ 'margin-top-3': index >= 1 }"

View file

@ -1,61 +1,97 @@
<template>
<form @submit.prevent>
<StyleBoxForm
v-for="{ name, label } in borders"
:key="name"
v-model="boxStyles[name]"
:label="label"
:padding-is-allowed="isStyleAllowed(`style_padding_${name}`)"
:border-is-allowed="isStyleAllowed(`style_border_${name}`)"
/>
<FormGroup
v-if="isStyleAllowed('style_background')"
class="margin-bottom-2"
small-label
required
:label="$t('defaultStyleForm.backgroundLabel')"
>
<Dropdown v-model="values.style_background">
<DropdownItem
v-for="type in Object.values(BACKGROUND_TYPES)"
:key="type.value"
:name="$t(type.name)"
:value="type.value"
<FormSection v-if="isStyleAllowed('style_background')">
<FormGroup
v-if="isStyleAllowed('style_background')"
:label="$t('defaultStyleForm.backgroundLabel')"
small-label
required
class="margin-bottom-1"
>
<RadioGroup
v-model="values.style_background"
type="button"
:options="backgroundTypes"
/>
</Dropdown>
<ColorInputGroup
</FormGroup>
<FormGroup
v-if="
values.style_background === BACKGROUND_TYPES.COLOR.value &&
values.style_background === BACKGROUND_TYPES.COLOR &&
isStyleAllowed('style_background_color')
"
v-model="values.style_background_color"
label-after
class="margin-bottom-1"
small-label
required
>
<ColorInput
v-model="values.style_background_color"
small
:color-variables="colorVariables"
/>
</FormGroup>
<FormGroup
v-if="isStyleAllowed('style_background_file')"
:label="$t('defaultStyleForm.backgroundImage')"
small-label
required
class="margin-top-2"
:label="$t('defaultStyleForm.backgroundColor')"
:color-variables="colorVariables"
>
<ImageInput v-model="values.style_background_file" />
</FormGroup>
<FormGroup
v-if="
isStyleAllowed('style_background_mode') &&
values.style_background_file
"
:label="$t('defaultStyleForm.backgroundImageMode')"
small-label
required
>
<RadioGroup
v-model="values.style_background_mode"
type="button"
:options="backgroundModes"
/>
</FormGroup>
</FormSection>
<FormSection v-if="isStyleAllowed('style_width')">
<FormGroup
:label="$t('defaultStyleForm.widthLabel')"
small-label
required
class="margin-bottom-1"
>
<Dropdown v-model="values.style_width">
<DropdownItem
v-for="type in Object.values(WIDTH_TYPES)"
:key="type.value"
:name="$t(type.name)"
:value="type.value"
>
</DropdownItem>
</Dropdown>
</FormGroup>
</FormSection>
<FormSection v-for="{ name, label } in borders" :key="name" :title="label">
<StyleBoxForm
v-model="boxStyles[name]"
:padding-is-allowed="isStyleAllowed(`style_padding_${name}`)"
:border-is-allowed="isStyleAllowed(`style_border_${name}`)"
:margin-is-allowed="isStyleAllowed(`style_margin_${name}`)"
/>
</FormGroup>
<FormGroup
v-if="isStyleAllowed('style_width')"
:label="$t('defaultStyleForm.widthLabel')"
small-label
required
>
<Dropdown v-model="values.style_width">
<DropdownItem
v-for="type in Object.values(WIDTH_TYPES)"
:key="type.value"
:name="$t(type.name)"
:value="type.value"
></DropdownItem> </Dropdown
></FormGroup>
</FormSection>
</form>
</template>
<script>
import StyleBoxForm from '@baserow/modules/builder/components/elements/components/forms/style/StyleBoxForm'
import styleForm from '@baserow/modules/builder/mixins/styleForm'
import { BACKGROUND_TYPES, WIDTH_TYPES } from '@baserow/modules/builder/enums'
import {
BACKGROUND_TYPES,
WIDTH_TYPES,
BACKGROUND_MODES,
} from '@baserow/modules/builder/enums'
import { IMAGE_FILE_TYPES } from '@baserow/modules/core/enums'
export default {
components: { StyleBoxForm },
@ -63,6 +99,37 @@ export default {
computed: {
BACKGROUND_TYPES: () => BACKGROUND_TYPES,
WIDTH_TYPES: () => WIDTH_TYPES,
backgroundTypes() {
return [
{
label: this.$t('backgroundTypes.none'),
value: BACKGROUND_TYPES.NONE,
},
{
label: this.$t('backgroundTypes.color'),
value: BACKGROUND_TYPES.COLOR,
},
]
},
backgroundModes() {
return [
{
label: this.$t('backgroundModes.fill'),
value: BACKGROUND_MODES.FILL,
},
{
label: this.$t('backgroundModes.fit'),
value: BACKGROUND_MODES.FIT,
},
{
label: this.$t('backgroundModes.tile'),
value: BACKGROUND_MODES.TILE,
},
]
},
IMAGE_FILE_TYPES() {
return IMAGE_FILE_TYPES
},
},
}
</script>

View file

@ -1,53 +1,52 @@
<template>
<form @submit.prevent>
<FormGroup
class="style-box-form__control margin-bottom-2"
:label="label"
v-if="borderIsAllowed"
horizontal
class="margin-bottom-1"
small-label
required
:error="error"
:label="$t('styleBoxForm.borderColor')"
>
<div
v-if="borderIsAllowed || paddingIsAllowed"
class="row margin-bottom-2"
style="--gap: 6px"
>
<div v-if="borderIsAllowed" class="col col-4">
<div class="margin-bottom-1">
{{ $t('styleBoxForm.borderLabel') }}
</div>
<FormInput
v-model="values.border_size"
size="large"
type="number"
:min="0"
:max="200"
:error="error"
@blur="$v.values.border_size.$touch()"
></FormInput>
</div>
<div v-if="paddingIsAllowed" class="col col-4">
<div class="margin-bottom-1">
{{ $t('styleBoxForm.paddingLabel') }}
</div>
<FormInput
v-model="values.padding"
size="large"
type="number"
:error="error"
@blur="$v.values.padding.$touch()"
></FormInput>
</div>
</div>
<ColorInputGroup
v-if="borderIsAllowed"
<ColorInput
v-model="values.border_color"
label-after
class="margin-top-2"
:label="$t('styleBoxForm.borderLabel')"
small
:color-variables="colorVariables"
/>
</FormGroup>
<FormGroup
v-if="borderIsAllowed"
class="margin-bottom-1"
small-label
required
:label="$t('styleBoxForm.borderLabel')"
horizontal
:error-message="sizeError"
>
<PixelValueSelector v-model="values.border_size" />
</FormGroup>
<FormGroup
v-if="paddingIsAllowed"
class="margin-bottom-1"
small-label
required
:label="$t('styleBoxForm.paddingLabel')"
horizontal
:error-message="paddingError"
>
<PixelValueSelector v-model="values.padding" />
</FormGroup>
<FormGroup
v-if="marginIsAllowed"
class="margin-bottom-1"
small-label
required
:label="$t('styleBoxForm.marginLabel')"
horizontal
:error-message="marginError"
>
<PixelValueSelector v-model="actualMargin" />
</FormGroup>
</form>
</template>
@ -56,15 +55,14 @@ import { required, integer, between } from 'vuelidate/lib/validators'
import form from '@baserow/modules/core/mixins/form'
import { themeToColorVariables } from '@baserow/modules/builder/utils/theme'
import PixelValueSelector from '@baserow/modules/builder/components/PixelValueSelector'
export default {
name: 'StyleBoxForm',
components: { PixelValueSelector },
mixins: [form],
inject: ['builder'],
props: {
label: {
type: String,
required: true,
},
value: {
type: Object,
required: true,
@ -74,6 +72,11 @@ export default {
required: false,
default: () => false,
},
marginIsAllowed: {
type: Boolean,
required: false,
default: () => false,
},
borderIsAllowed: {
type: Boolean,
required: false,
@ -83,6 +86,7 @@ export default {
data() {
return {
values: {
margin: 0,
padding: 0,
border_color: 'border',
border_size: 0,
@ -90,15 +94,38 @@ export default {
}
},
computed: {
// TODO zdm can be removed when we remove the null value from backend field
actualMargin: {
get() {
return this.values.margin || 0
},
set(newValue) {
this.values.margin = newValue
},
},
colorVariables() {
return themeToColorVariables(this.builder.theme)
},
/**
* Returns only one error because we don't have the space to write one error per
* field as the style fields are on the same line.
*/
error() {
return this.$v.values.padding.$error || this.$v.values.border_size.$error
marginError() {
if (this.$v.actualMargin.$invalid) {
return this.$t('error.minMaxValueField', { min: 0, max: 200 })
} else {
return ''
}
},
paddingError() {
if (this.$v.values.padding.$invalid) {
return this.$t('error.minMaxValueField', { min: 0, max: 200 })
} else {
return ''
}
},
sizeError() {
if (this.$v.values.border_size.$invalid) {
return this.$t('error.minMaxValueField', { min: 0, max: 200 })
} else {
return ''
}
},
},
methods: {
@ -123,6 +150,11 @@ export default {
between: between(0, 200),
},
},
actualMargin: {
required,
integer,
between: between(0, 200),
},
}
},
}

View file

@ -1,5 +1,5 @@
<template>
<ThemeProvider>
<ThemeProvider class="page">
<PageElement
v-for="element in elements"
:key="element.id"

View file

@ -3,16 +3,18 @@
v-if="elementMode === 'editing' || isVisible"
class="element__wrapper"
:class="{
'element__wrapper--full-width':
'element__wrapper--full-bleed':
element.style_width === WIDTH_TYPES.FULL.value,
'element__wrapper--full-width':
element.style_width === WIDTH_TYPES.FULL_WIDTH.value,
'element__wrapper--medium-width':
element.style_width === WIDTH_TYPES.MEDIUM.value,
'element__wrapper--small-width':
element.style_width === WIDTH_TYPES.SMALL.value,
}"
:style="wrapperStyles"
:style="elementStyles"
>
<div class="element__inner-wrapper" :style="innerWrapperStyles">
<div class="element__inner-wrapper">
<component
:is="component"
:element="element"
@ -27,11 +29,14 @@
</template>
<script>
import _ from 'lodash'
import { resolveColor } from '@baserow/modules/core/utils/colors'
import { themeToColorVariables } from '@baserow/modules/builder/utils/theme'
import { BACKGROUND_TYPES, WIDTH_TYPES } from '@baserow/modules/builder/enums'
import {
BACKGROUND_TYPES,
WIDTH_TYPES,
BACKGROUND_MODES,
} from '@baserow/modules/builder/enums'
import applicationContextMixin from '@baserow/modules/builder/mixins/applicationContext'
import {
VISIBILITY_NOT_LOGGED,
@ -107,109 +112,75 @@ export default {
return true
}
},
allowedStyles() {
const parentElement = this.$store.getters['element/getElementById'](
this.page,
this.element.parent_element_id
)
const elementType = this.$registry.get('element', this.element.type)
const parentElementType = this.parentElement
? this.$registry.get('element', parentElement?.type)
: null
return !parentElementType
? elementType.styles
: _.difference(
elementType.styles,
parentElementType.childStylesForbidden
)
},
/**
* Computes an object containing all the style properties that must be set on
* the element wrapper.
*/
wrapperStyles() {
elementStyles() {
const styles = {
style_background_color: {
'--background-color':
this.element.style_background === BACKGROUND_TYPES.COLOR.value
? this.resolveColor(
this.element.style_background_color,
this.colorVariables
)
: 'transparent',
},
style_border_top: {
'--border-top': this.border(
this.element.style_border_top_size,
this.element.style_border_top_color
),
},
style_border_bottom: {
'--border-bottom': this.border(
this.element.style_border_bottom_size,
this.element.style_border_bottom_color
),
},
style_border_left: {
'--border-left': this.border(
this.element.style_border_left_size,
this.element.style_border_left_color
),
},
style_border_right: {
'--border-right': this.border(
this.element.style_border_right_size,
this.element.style_border_right_color
),
},
'--element-background-color':
this.element.style_background === BACKGROUND_TYPES.COLOR
? this.resolveColor(
this.element.style_background_color,
this.colorVariables
)
: 'none',
'--element-background-image':
this.element.style_background_file !== null
? `url(${this.element.style_background_file.url})`
: 'none',
'--element-border-top': this.border(
this.element.style_border_top_size,
this.element.style_border_top_color
),
'--element-margin-top': `${this.element.style_margin_top || 0}px`,
'--element-padding-top': `${this.element.style_padding_top || 0}px`,
'--element-border-bottom': this.border(
this.element.style_border_bottom_size,
this.element.style_border_bottom_color
),
'--element-margin-bottom': `${this.element.style_margin_bottom || 0}px`,
'--element-padding-bottom': `${
this.element.style_padding_bottom || 0
}px`,
'--element-border-left': this.border(
this.element.style_border_left_size,
this.element.style_border_left_color
),
'--element-margin-left': `${this.element.style_margin_left || 0}px`,
'--element-padding-left': `${this.element.style_padding_left || 0}px`,
'--element-border-right': this.border(
this.element.style_border_right_size,
this.element.style_border_right_color
),
'--element-margin-right': `${this.element.style_margin_right || 0}px`,
'--element-padding-right': `${this.element.style_padding_right || 0}px`,
}
return Object.keys(styles).reduce((acc, key) => {
if (this.allowedStyles.includes(key)) {
acc = { ...acc, ...styles[key] }
if (this.element.style_background_file !== null) {
if (this.element.style_background_mode === BACKGROUND_MODES.FILL) {
styles['--element-background-size'] = 'cover'
styles['--element-background-repeat'] = 'no-repeat'
}
if (this.element.style_background_mode === BACKGROUND_MODES.TILE) {
styles['--element-background-size'] = 'auto'
styles['--element-background-repeat'] = 'repeat'
}
if (this.element.style_background_mode === BACKGROUND_MODES.FIT) {
styles['--element-background-size'] = 'contain'
styles['--element-background-repeat'] = 'no-repeat'
}
return acc
}, {})
},
/**
* Computes an object containing all the style properties that must be set on
* the element inner wrapper.
*/
innerWrapperStyles() {
const styles = {
style_padding_top: {
'--padding-top': `${this.element.style_padding_top || 0}px`,
},
style_padding_bottom: {
'--padding-bottom': `${this.element.style_padding_bottom || 0}px`,
},
style_padding_left: {
'--padding-left': `${this.element.style_padding_left || 0}px`,
},
style_padding_right: {
'--padding-right': `${this.element.style_padding_right || 0}px`,
},
}
return Object.keys(styles).reduce((acc, key) => {
if (this.allowedStyles.includes(key)) {
acc = { ...acc, ...styles[key] }
}
return acc
}, {})
return styles
},
},
methods: {
resolveColor,
border(size, color) {
return `solid ${size || 0}px ${this.resolveColor(
color,
this.colorVariables
)}`
if (!size) {
return 'none'
}
return `solid ${size}px ${this.resolveColor(color, this.colorVariables)}`
},
},
}

View file

@ -23,21 +23,23 @@
>
{{ $t('pagePreview.emptyMessage') }}
</CallToAction>
<AddElementModal ref="addElementModal" :page="page" />
<ElementPreview
v-for="(element, index) in elements"
:key="element.id"
is-root-element
:element="element"
:is-first-element="index === 0"
:is-last-element="index === elements.length - 1"
:is-copying="copyingElementIndex === index"
:application-context-additions="{
recordIndexPath: [],
}"
@move="moveElement($event)"
/>
<div class="page">
<ElementPreview
v-for="(element, index) in elements"
:key="element.id"
is-root-element
:element="element"
:is-first-element="index === 0"
:is-last-element="index === elements.length - 1"
:is-copying="copyingElementIndex === index"
:application-context-additions="{
recordIndexPath: [],
}"
@move="moveElement($event)"
/>
</div>
</div>
<AddElementModal ref="addElementModal" :page="page" />
</div>
</ThemeProvider>
</template>

View file

@ -12,9 +12,8 @@
<WidthSelector v-model="buttonWidth" />
<template #after-input>
<ResetButton
v-model="values"
:theme="theme"
property="button_width"
v-model="values.button_width"
:default-value="theme?.button_width"
/>
</template>
</FormGroup>
@ -29,9 +28,8 @@
<HorizontalAlignmentsSelector v-model="values.button_alignment" />
<template #after-input>
<ResetButton
v-model="values"
:theme="theme"
property="button_alignment"
v-model="values.button_alignment"
:default-value="theme?.button_alignment"
/>
</template>
</FormGroup>
@ -48,12 +46,95 @@
/>
<template #after-input>
<ResetButton
v-model="values"
:theme="theme"
property="button_text_alignment"
v-model="values.button_text_alignment"
:default-value="theme?.button_text_alignment"
/>
</template>
</FormGroup>
<FormGroup
horizontal
small-label
:label="$t('buttonThemeConfigBlock.fontFamily')"
class="margin-bottom-1"
>
<FontFamilySelector v-model="values.button_font_family" />
<template #after-input>
<ResetButton
v-model="values.button_font_family"
:default-value="theme?.button_font_family"
/>
</template>
</FormGroup>
<FormGroup
horizontal
small-label
:label="$t('buttonThemeConfigBlock.size')"
:error-message="getError('button_font_size')"
class="margin-bottom-1"
>
<PixelValueSelector v-model="values.button_font_size" />
<template #after-input>
<ResetButton
v-model="values.button_font_size"
:default-value="theme?.button_font_size"
/>
</template>
</FormGroup>
<FormGroup
horizontal
small-label
:label="$t('buttonThemeConfigBlock.borderSize')"
:error-message="getError('button_border_size')"
class="margin-bottom-1"
>
<PixelValueSelector v-model="values.button_border_size" />
<template #after-input>
<ResetButton
v-model="values.button_border_size"
:default-value="theme?.button_border_size"
/>
</template>
</FormGroup>
<FormGroup
horizontal
small-label
:label="$t('buttonThemeConfigBlock.borderRadius')"
:error-message="getError('button_border_radius')"
class="margin-bottom-1"
>
<PixelValueSelector v-model="values.button_border_radius" />
<template #after-input>
<ResetButton
v-model="values.button_border_radius"
:default-value="theme?.button_border_radius"
/>
</template>
</FormGroup>
<FormGroup
horizontal
small-label
:label="$t('buttonThemeConfigBlock.padding')"
:error-message="getPaddingError()"
class="margin-bottom-1"
>
<PaddingSelector v-model="padding" />
<template #after-input>
<ResetButton
v-model="padding"
:default-value="
theme
? {
vertical: theme['button_vertical_padding'],
horizontal: theme['button_horizontal_padding'],
}
: undefined
"
/>
</template>
</FormGroup>
</template>
<template #preview>
<ABButton>{{ $t('buttonThemeConfigBlock.button') }}</ABButton>
</template>
</ThemeConfigBlockSection>
<ThemeConfigBlockSection :title="$t('buttonThemeConfigBlock.defaultState')">
@ -73,9 +154,48 @@
/>
<template #after-input>
<ResetButton
v-model="values"
:theme="theme"
property="button_background_color"
v-model="values.button_background_color"
:default-value="theme?.button_background_color"
/>
</template>
</FormGroup>
<FormGroup
horizontal
small-label
required
class="margin-bottom-1"
:label="$t('buttonThemeConfigBlock.textColor')"
>
<ColorInput
v-model="values.button_text_color"
:color-variables="colorVariables"
:default-value="theme?.button_text_color"
small
/>
<template #after-input>
<ResetButton
v-model="values.button_text_color"
:default-value="theme?.button_text_color"
/>
</template>
</FormGroup>
<FormGroup
horizontal
small-label
required
class="margin-bottom-1"
:label="$t('buttonThemeConfigBlock.borderColor')"
>
<ColorInput
v-model="values.button_border_color"
:color-variables="colorVariables"
:default-value="theme?.button_border_color"
small
/>
<template #after-input>
<ResetButton
v-model="values.button_border_color"
:default-value="theme?.button_border_color"
/>
</template>
</FormGroup>
@ -101,9 +221,46 @@
/>
<template #after-input>
<ResetButton
v-model="values"
:theme="theme"
property="button_hover_background_color"
v-model="values.button_hover_background_color"
:default-value="theme?.button_hover_background_color"
/>
</template>
</FormGroup>
<FormGroup
horizontal
small-label
class="margin-bottom-1"
:label="$t('buttonThemeConfigBlock.textColor')"
>
<ColorInput
v-model="values.button_hover_text_color"
:color-variables="colorVariables"
:default-value="theme?.button_hover_text_color"
small
/>
<template #after-input>
<ResetButton
v-model="values.button_hover_text_color"
:default-value="theme?.button_hover_text_color"
/>
</template>
</FormGroup>
<FormGroup
horizontal
small-label
class="margin-bottom-1"
:label="$t('buttonThemeConfigBlock.borderColor')"
>
<ColorInput
v-model="values.button_hover_border_color"
:color-variables="colorVariables"
:default-value="theme?.button_hover_border_color"
small
/>
<template #after-input>
<ResetButton
v-model="values.button_hover_border_color"
:default-value="theme?.button_hover_border_color"
/>
</template>
</FormGroup>
@ -123,6 +280,36 @@ import ThemeConfigBlockSection from '@baserow/modules/builder/components/theme/T
import ResetButton from '@baserow/modules/builder/components/theme/ResetButton'
import HorizontalAlignmentsSelector from '@baserow/modules/builder/components/HorizontalAlignmentsSelector'
import WidthSelector from '@baserow/modules/builder/components/WidthSelector'
import FontFamilySelector from '@baserow/modules/builder/components/FontFamilySelector'
import PixelValueSelector from '@baserow/modules/builder/components/PixelValueSelector'
import PaddingSelector from '@baserow/modules/builder/components/PaddingSelector'
import { required, integer, minValue, maxValue } from 'vuelidate/lib/validators'
const pixelSizeMin = 1
const pixelSizeMax = 100
const minMax = {
button_font_size: {
min: 1,
max: 100,
},
button_border_size: {
min: 0,
max: 100,
},
button_border_radius: {
min: 0,
max: 100,
},
button_horizontal_padding: {
min: 0,
max: 100,
},
button_vertical_padding: {
min: 0,
max: 100,
},
}
export default {
name: 'ButtonThemeConfigBlock',
@ -131,18 +318,14 @@ export default {
ResetButton,
WidthSelector,
HorizontalAlignmentsSelector,
FontFamilySelector,
PixelValueSelector,
PaddingSelector,
},
mixins: [themeConfigBlock],
data() {
return {
values: {},
allowedValues: [
'button_background_color',
'button_hover_background_color',
'button_text_alignment',
'button_alignment',
'button_width',
],
}
},
computed: {
@ -161,6 +344,73 @@ export default {
}
},
},
padding: {
get() {
return {
vertical: this.values.button_vertical_padding,
horizontal: this.values.button_horizontal_padding,
}
},
set(newValue) {
this.values.button_vertical_padding = newValue.vertical
this.values.button_horizontal_padding = newValue.horizontal
},
},
pixedSizeMin() {
return pixelSizeMin
},
pixedSizeMax() {
return pixelSizeMax
},
},
methods: {
isAllowedKey(key) {
return key.startsWith('button_')
},
getError(property) {
if (this.$v.values[property].$invalid) {
return this.$t('error.minMaxValueField', minMax[property])
}
return null
},
getPaddingError() {
return (
this.getError('button_vertical_padding') ||
this.getError('button_horizontal_padding')
)
},
},
validations: {
values: {
button_font_size: {
required,
integer,
minValue: minValue(minMax.button_font_size.min),
maxValue: maxValue(minMax.button_font_size.max),
},
button_border_size: {
integer,
minValue: minValue(minMax.button_border_size.min),
maxValue: maxValue(minMax.button_border_size.max),
},
button_border_radius: {
integer,
minValue: minValue(minMax.button_border_radius.min),
maxValue: maxValue(minMax.button_border_radius.max),
},
button_horizontal_padding: {
required,
integer,
minValue: minValue(minMax.button_horizontal_padding.min),
maxValue: maxValue(minMax.button_horizontal_padding.max),
},
button_vertical_padding: {
required,
integer,
minValue: minValue(minMax.button_vertical_padding.min),
maxValue: maxValue(minMax.button_vertical_padding.max),
},
},
},
}
</script>

View file

@ -1,15 +1,54 @@
<template>
<ThemeConfigBlockSection>
<template #default>
<FormGroup horizontal :label="$t('colorThemeConfigBlock.primaryColor')">
<FormGroup
horizontal
small-label
class="margin-bottom-1"
:label="$t('colorThemeConfigBlock.primaryColor')"
>
<ColorInput v-model="values.primary_color" small />
</FormGroup>
<FormGroup horizontal :label="$t('colorThemeConfigBlock.secondaryColor')">
<FormGroup
horizontal
small-label
class="margin-bottom-1"
:label="$t('colorThemeConfigBlock.secondaryColor')"
>
<ColorInput v-model="values.secondary_color" small />
</FormGroup>
<FormGroup horizontal :label="$t('colorThemeConfigBlock.borderColor')">
<FormGroup
horizontal
small-label
class="margin-bottom-1"
:label="$t('colorThemeConfigBlock.borderColor')"
>
<ColorInput v-model="values.border_color" small />
</FormGroup>
<FormGroup
horizontal
small-label
class="margin-bottom-1"
:label="$t('colorThemeConfigBlock.successColor')"
>
<ColorInput v-model="values.main_success_color" small />
</FormGroup>
<FormGroup
horizontal
small-label
class="margin-bottom-1"
:label="$t('colorThemeConfigBlock.warningColor')"
>
<ColorInput v-model="values.main_warning_color" small />
</FormGroup>
<FormGroup
horizontal
small-label
class="margin-bottom-1"
:label="$t('colorThemeConfigBlock.errorColor')"
>
<ColorInput v-model="values.main_error_color" small />
</FormGroup>
</template>
</ThemeConfigBlockSection>
</template>
@ -26,8 +65,15 @@ export default {
data() {
return {
values: {},
allowedValues: ['primary_color', 'secondary_color', 'border_color'],
}
},
methods: {
isAllowedKey(key) {
return (
key.startsWith('main_') ||
['primary_color', 'secondary_color', 'border_color'].includes(key)
)
},
},
}
</script>

View file

@ -13,9 +13,8 @@
<template #after-input>
<ResetButton
v-model="values"
:theme="theme"
property="image_alignment"
v-model="values.image_alignment"
:default-value="theme?.image_alignment"
/>
</template>
</FormGroup>
@ -30,7 +29,8 @@
v-model="values.image_max_width"
small
type="number"
:error="
remove-number-input-controls
:error-message="
$v.values.image_max_width.$dirty &&
!$v.values.image_max_width.integer
? $t('error.integerField')
@ -50,9 +50,8 @@
<template #after-input>
<ResetButton
v-model="values"
:theme="theme"
property="image_max_width"
v-model="values.image_max_width"
:default-value="theme?.image_max_width"
/>
</template>
</FormGroup>
@ -67,7 +66,8 @@
v-model="imageMaxHeight"
small
type="number"
:error="
remove-number-input-controls
:error-message="
$v.values.image_max_height.$dirty &&
!$v.values.image_max_height.integer
? $t('error.integerField')
@ -85,9 +85,8 @@
<template #after-input>
<ResetButton
v-model="values"
:theme="theme"
property="image_max_height"
v-model="imageMaxHeight"
:default-value="theme?.image_max_height"
/>
</template>
</FormGroup>
@ -122,8 +121,7 @@
<template #after-input>
<ResetButton
v-model="imageConstraintForReset"
:theme="theme"
property="image_constraint"
:default-value="theme?.image_constraint"
/>
</template>
</FormGroup>
@ -191,18 +189,18 @@ export default {
},
imageConstraintForReset: {
get() {
return { image_constraint: this.values.image_constraint }
return this.values.image_constraint
},
set(value) {
if (value.image_constraint === 'contain') {
if (value === 'contain') {
// Reset the height as we can't have a max height with contain
this.values.image_max_height = null
}
if (value.image_constraint === 'cover') {
if (value === 'cover') {
// Set the height to what is defined in theme
this.values.image_max_height = this.theme.image_max_height
}
this.values.image_constraint = value.image_constraint
this.values.image_constraint = value
},
},
IMAGE_SOURCE_TYPES() {
@ -235,6 +233,9 @@ export default {
)
}
},
isAllowedKey(key) {
return key.startsWith('image_')
},
},
validations: {
values: {

View file

@ -6,15 +6,29 @@
horizontal
small-label
required
:label="$t('linkThemeConfigBlock.alignment')"
class="margin-bottom-1"
:label="$t('linkThemeConfigBlock.fontFamily')"
>
<FontFamilySelector v-model="values.link_font_family" />
<template #after-input>
<ResetButton
v-model="values.link_font_family"
:default-value="theme?.link_font_family"
/>
</template>
</FormGroup>
<FormGroup
horizontal
small-label
required
class="margin-bottom-1"
:label="$t('linkThemeConfigBlock.alignment')"
>
<HorizontalAlignmentsSelector v-model="values.link_text_alignment" />
<template #after-input>
<ResetButton
v-model="values"
:theme="theme"
property="link_text_alignment"
v-model="values.link_text_alignment"
:default-value="theme?.link_text_alignment"
/>
</template>
</FormGroup>
@ -37,9 +51,8 @@
/>
<template #after-input>
<ResetButton
v-model="values"
:theme="theme"
property="link_text_color"
v-model="values.link_text_color"
:default-value="theme?.link_text_color"
/>
</template>
</FormGroup>
@ -65,9 +78,8 @@
/>
<template #after-input>
<ResetButton
v-model="values"
:theme="theme"
property="link_hover_text_color"
v-model="values.link_hover_text_color"
:default-value="theme?.link_hover_text_color"
/>
</template>
</FormGroup>
@ -86,6 +98,7 @@ import themeConfigBlock from '@baserow/modules/builder/mixins/themeConfigBlock'
import ThemeConfigBlockSection from '@baserow/modules/builder/components/theme/ThemeConfigBlockSection'
import ResetButton from '@baserow/modules/builder/components/theme/ResetButton'
import HorizontalAlignmentsSelector from '@baserow/modules/builder/components/HorizontalAlignmentsSelector'
import FontFamilySelector from '@baserow/modules/builder/components/FontFamilySelector'
export default {
name: 'LinkThemeConfigBlock',
@ -93,17 +106,18 @@ export default {
ThemeConfigBlockSection,
ResetButton,
HorizontalAlignmentsSelector,
FontFamilySelector,
},
mixins: [themeConfigBlock],
data() {
return {
values: {},
allowedValues: [
'link_text_color',
'link_hover_text_color',
'link_text_alignment',
],
}
},
methods: {
isAllowedKey(key) {
return key.startsWith('link_')
},
},
}
</script>

View file

@ -1,215 +0,0 @@
<template>
<div>
<ColorPickerContext
ref="colorPicker"
:value="builder.theme[colorPickerPropertyName] || '#000000ff'"
@input="colorPickerColorChanged"
></ColorPickerContext>
<div class="theme-settings__section margin-bottom-3">
<div class="theme-settings__section-properties">
<a
class="theme-settings__section-title"
@click="toggleClosed('colors')"
>
{{ $t('mainThemeConfigBlock.colorsLabel') }}
<i
class="iconoir-nav-arrow-down theme-settings__section-title-icon"
:class="{
'theme-settings__section-title-icon': true,
'iconoir-nav-arrow-down': !isClosed('colors'),
'iconoir-nav-arrow-right': isClosed('colors'),
}"
></i>
</a>
<div v-show="!isClosed('colors')">
<ColorInputGroup
:value="builder.theme.primary_color"
label-after
:label="$t('mainThemeConfigBlock.primaryColor')"
class="margin-bottom-1"
@input="setPropertyInStore('primary_color', $event)"
/>
<ColorInputGroup
:value="builder.theme.secondary_color"
label-after
:label="$t('mainThemeConfigBlock.secondaryColor')"
@input="setPropertyInStore('secondary_color', $event)"
/>
</div>
</div>
</div>
<div>
<div class="theme-settings__section">
<div class="theme-settings__section-properties">
<a
class="theme-settings__section-title"
@click="toggleClosed('typography')"
>
{{ $t('mainThemeConfigBlock.typography') }}
<i
class="iconoir-nav-arrow-down theme-settings__section-title-icon"
:class="{
'theme-settings__section-title-icon': true,
'iconoir-nav-arrow-down': !isClosed('typography'),
'iconoir-nav-arrow-right': isClosed('typography'),
}"
></i>
</a>
</div>
</div>
<div
v-for="i in headings"
v-show="!isClosed('typography')"
:key="i"
class="theme-settings__section"
>
<div class="theme-settings__section-properties">
<FormGroup
:label="$t('mainThemeConfigBlock.headingLabel', { i })"
small-label
required
:error="$v.builder.theme[`heading_${i}_font_size`].$error"
class="margin-bottom-2"
>
<div class="theme-settings__inputs-wrapper">
<ColorInput
:value="builder.theme[`heading_${i}_color`]"
@input="setPropertyInStore(`heading_${i}_color`, $event)"
/>
<FormInput
class="theme-settings__input-font-size"
type="number"
size="large"
remove-number-input-controls
:min="fontSizeMin"
:max="fontSizeMax"
:value="builder.theme[`heading_${i}_font_size`]"
:error="$v.builder.theme[`heading_${i}_font_size`].$error"
@input="
;[
$v.builder.theme[`heading_${i}_font_size`].$touch(),
setPropertyInStore(
`heading_${i}_font_size`,
$event,
!$v.builder.theme[`heading_${i}_font_size`].$error
),
]
"
>
<template #suffix>px</template></FormInput
>
</div>
<template #error>
{{ $t('error.minMaxLength', { min: 1, max: 100 }) }}
</template>
</FormGroup>
</div>
<div class="theme-settings__section-preview">
<component
:is="`h${i}`"
class="margin-bottom-2 theme-settings__section-ellipsis"
:class="`ab-heading--h${i}`"
:style="{
[`--heading-h${i}-color`]: builder.theme[`heading_${i}_color`],
[`--heading-h${i}-font-size`]:
builder.theme[`heading_${i}_font_size`] + 'px',
}"
>
{{ $t('mainThemeConfigBlock.headingValue', { i }) }}
</component>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapActions } from 'vuex'
import { required, integer, minValue, maxValue } from 'vuelidate/lib/validators'
import ColorPickerContext from '@baserow/modules/core/components/ColorPickerContext'
import { notifyIf } from '@baserow/modules/core/utils/error'
const fontSizeMin = 1
const fontSizeMax = 100
const headings = [1, 2, 3]
export default {
name: 'MainThemeConfigBlock',
components: { ColorPickerContext },
props: {
builder: {
type: Object,
required: true,
},
},
data() {
return {
closed: [],
colorPickerPropertyName: '',
}
},
computed: {
headings() {
return headings
},
fontSizeMin() {
return fontSizeMin
},
fontSizeMax() {
return fontSizeMax
},
},
methods: {
...mapActions({
setThemeProperty: 'theme/setProperty',
forceSetThemeProperty: 'theme/forceSetProperty',
}),
toggleClosed(value) {
const index = this.closed.indexOf(value)
if (index < 0) {
this.closed.push(value)
} else {
this.closed.splice(index, 1)
}
},
isClosed(value) {
return this.closed.includes(value)
},
openColorPicker(opener, propertyName) {
this.colorPickerPropertyName = propertyName
this.$refs.colorPicker.toggle(opener)
},
colorPickerColorChanged(value) {
this.setPropertyInStore(this.colorPickerPropertyName, value)
},
async setPropertyInStore(key, value, makeRequest = true) {
const action = makeRequest ? 'setThemeProperty' : 'forceSetThemeProperty'
try {
await this[action]({
builder: this.builder,
key,
value,
})
} catch (error) {
notifyIf(error, 'row')
}
},
},
validations: {
builder: {
theme: headings.reduce((o, i) => {
o[`heading_${i}_font_size`] = {
required,
integer,
minValue: minValue(fontSizeMin),
maxValue: maxValue(fontSizeMax),
}
return o
}, {}),
},
},
}
</script>

View file

@ -0,0 +1,80 @@
<template>
<ThemeConfigBlockSection>
<template #default>
<FormGroup
horizontal
small-label
class="margin-bottom-1"
:label="$t('pageThemeConfigBlock.backgroundColor')"
>
<ColorInput
v-model="values.page_background_color"
small
:allow-opacity="false"
:color-variables="colorVariables"
/>
</FormGroup>
<FormGroup
horizontal
small-label
class="margin-bottom-1"
:label="$t('pageThemeConfigBlock.backgroundImage')"
>
<ImageInput v-model="values.page_background_file" />
</FormGroup>
<FormGroup
v-if="values.page_background_file"
horizontal
small-label
class="margin-bottom-1"
:label="$t('pageThemeConfigBlock.backgroundMode')"
>
<RadioGroup
v-model="values.page_background_mode"
type="button"
:options="backgroundModes"
/>
</FormGroup>
</template>
</ThemeConfigBlockSection>
</template>
<script>
import themeConfigBlock from '@baserow/modules/builder/mixins/themeConfigBlock'
import ThemeConfigBlockSection from '@baserow/modules/builder/components/theme/ThemeConfigBlockSection'
import { BACKGROUND_MODES } from '@baserow/modules/builder/enums'
export default {
name: 'PageThemeConfigBlock',
components: { ThemeConfigBlockSection },
mixins: [themeConfigBlock],
data() {
return {
values: {},
}
},
computed: {
backgroundModes() {
return [
{
label: this.$t('backgroundModes.tile'),
value: BACKGROUND_MODES.TILE,
},
{
label: this.$t('backgroundModes.fill'),
value: BACKGROUND_MODES.FILL,
},
{
label: this.$t('backgroundModes.fit'),
value: BACKGROUND_MODES.FIT,
},
]
},
},
methods: {
isAllowedKey(key) {
return key.startsWith('page_')
},
},
}
</script>

View file

@ -13,25 +13,25 @@ import _ from 'lodash'
export default {
inject: ['builder'],
props: {
theme: { type: Object, required: false, default: null },
property: { type: String, required: true },
value: { type: Object, required: true },
value: { required: true, validator: (v) => true },
defaultValue: {
required: false,
validator: (v) => true,
default: undefined,
},
},
data() {
return {}
},
methods: {
propertyModified() {
if (!this.theme) {
return false
}
return !_.isEqual(this.value[this.property], this.theme[this.property])
return (
this.defaultValue !== undefined &&
!_.isEqual(this.value, this.defaultValue)
)
},
resetProperty() {
this.$emit('input', {
...this.value,
[this.property]: this.theme[this.property],
})
this.$emit('input', this.defaultValue)
},
},
}

View file

@ -3,54 +3,66 @@
<ThemeConfigBlockSection
v-if="showBody"
:title="$t('typographyThemeConfigBlock.bodyLabel')"
class="margin-bottom-2"
>
<template #default>
<FormGroup
horizontal
small-label
class="margin-bottom-1"
:label="$t('typographyThemeConfigBlock.fontFamily')"
>
<FontFamilySelector v-model="values.body_font_family" />
<template #after-input>
<ResetButton
v-model="values.body_font_family"
:default-value="theme?.body_font_family"
/>
</template>
</FormGroup>
<FormGroup
horizontal
small-label
class="margin-bottom-1"
:label="$t('typographyThemeConfigBlock.textAlignment')"
>
<HorizontalAlignmentsSelector
v-model="values[`body_text_alignment`]"
<HorizontalAlignmentsSelector v-model="values.body_text_alignment" />
<template #after-input>
<ResetButton
v-model="values.body_text_alignment"
:default-value="theme?.body_text_alignment"
/>
</template>
</FormGroup>
<FormGroup
horizontal
small-label
class="margin-bottom-1"
:label="$t('typographyThemeConfigBlock.size')"
:error-message="
$v.values[`body_font_size`].$invalid
? $t('error.minMaxValueField', {
min: fontSizeMin,
max: bodyFontSizeMax,
})
: ''
"
>
<PixelValueSelector
v-model="values.body_font_size"
class="typography-theme-config-block__input-number"
@blur="$v.values[`body_font_size`].$touch()"
/>
<template #after-input>
<ResetButton
v-model="values"
:theme="theme"
:property="`body_text_alignment`"
/>
</template>
</FormGroup>
<FormGroup
horizontal
small-label
:label="$t('typographyThemeConfigBlock.size')"
:error="$v.values[`body_font_size`].$invalid"
>
<FormInput
v-model="values[`body_font_size`]"
type="number"
remove-number-input-controls
:min="fontSizeMin"
:max="fontSizeMax"
:error="$v.values[`body_font_size`].$invalid"
@blur="$v.values[`body_font_size`].$touch()"
>
<template #suffix>px</template>
</FormInput>
<template #after-input>
<ResetButton
v-model="values"
:theme="theme"
:property="`body_font_size`"
v-model="values.body_font_size"
:default-value="theme?.body_font_size"
/>
</template>
</FormGroup>
<FormGroup
horizontal
small-label
class="margin-bottom-1"
:label="$t('typographyThemeConfigBlock.color')"
>
<ColorInput
@ -61,9 +73,8 @@
/>
<template #after-input>
<ResetButton
v-model="values"
:theme="theme"
:property="'body_text_color'"
v-model="values.body_text_color"
:default-value="theme?.body_text_color"
/>
</template>
</FormGroup>
@ -75,87 +86,103 @@
</ABParagraph>
</template>
</ThemeConfigBlockSection>
<ThemeConfigBlockSection
v-for="level in headings"
:key="level"
:title="$t('typographyThemeConfigBlock.headingLabel', { i: level })"
class="margin-bottom-2"
>
<template #default>
<FormGroup
horizontal
small-label
:label="$t('typographyThemeConfigBlock.textAlignment')"
>
<HorizontalAlignmentsSelector
v-model="values[`heading_${level}_text_alignment`]"
/>
<template #after-input>
<ResetButton
v-model="values"
:theme="theme"
:property="`heading_${level}_text_alignment`"
/>
</template>
</FormGroup>
<FormGroup
horizontal
small-label
:label="$t('typographyThemeConfigBlock.size')"
:error="$v.values[`heading_${level}_font_size`].$invalid"
>
<FormInput
v-model="values[`heading_${level}_font_size`]"
type="number"
remove-number-input-controls
:min="fontSizeMin"
:max="fontSizeMax"
:error="$v.values[`heading_${level}_font_size`].$invalid"
@blur="$v.values[`heading_${level}_font_size`].$touch()"
<template v-if="showHeadings">
<ThemeConfigBlockSection
v-for="level in headings"
:key="level"
:title="$t('typographyThemeConfigBlock.headingLabel', { i: level })"
>
<template #default>
<FormGroup
horizontal
small-label
class="margin-bottom-1"
:label="$t('typographyThemeConfigBlock.fontFamily')"
>
<template #suffix>px</template>
</FormInput>
<template #after-input>
<ResetButton
v-model="values"
:theme="theme"
:property="`body_font_size`"
<FontFamilySelector
v-model="values[`heading_${level}_font_family`]"
/>
</template>
</FormGroup>
<FormGroup
horizontal
small-label
:label="$t('typographyThemeConfigBlock.color')"
>
<ColorInput
v-model="values[`heading_${level}_text_color`]"
:color-variables="colorVariables"
:default-value="theme ? theme[`heading_${level}_text_color`] : null"
small
/>
<template #after-input>
<ResetButton
v-model="values"
:theme="theme"
:property="`heading_${level}_text_color`"
<template #after-input>
<ResetButton
v-model="values[`heading_${level}_font_family`]"
:default-value="theme?.[`heading_${level}_font_family`]"
/>
</template>
</FormGroup>
<FormGroup
horizontal
small-label
class="margin-bottom-1"
:label="$t('typographyThemeConfigBlock.textAlignment')"
>
<HorizontalAlignmentsSelector
v-model="values[`heading_${level}_text_alignment`]"
/>
</template>
</FormGroup>
</template>
<template #preview>
<component
:is="`h${level}`"
class="margin-bottom-2 theme-settings__section-ellipsis"
:class="`ab-heading--h${level}`"
>
{{ $t('typographyThemeConfigBlock.headingValue', { i: level }) }}
</component>
</template>
</ThemeConfigBlockSection>
<template #after-input>
<ResetButton
v-model="values[`heading_${level}_text_alignment`]"
:default-value="theme?.[`heading_${level}_text_alignment`]"
/>
</template>
</FormGroup>
<FormGroup
horizontal
small-label
class="margin-bottom-1"
:label="$t('typographyThemeConfigBlock.size')"
:error-message="
$v.values[`heading_${level}_font_size`].$invalid
? $t('error.minMaxValueField', {
min: fontSizeMin,
max: fontSizeMax,
})
: ''
"
>
<PixelValueSelector
v-model="values[`heading_${level}_font_size`]"
class="typography-theme-config-block__input-number"
@blur="$v.values[`heading_${level}_font_size`].$touch()"
/>
<template #after-input>
<ResetButton
v-model="values[`heading_${level}_font_size`]"
:default-value="theme?.[`heading_${level}_font_size`]"
/>
</template>
</FormGroup>
<FormGroup
horizontal
small-label
class="margin-bottom-1"
:label="$t('typographyThemeConfigBlock.color')"
>
<ColorInput
v-model="values[`heading_${level}_text_color`]"
:color-variables="colorVariables"
:default-value="
theme ? theme[`heading_${level}_text_color`] : null
"
small
/>
<template #after-input>
<ResetButton
v-model="values[`heading_${level}_text_color`]"
:default-value="theme?.[`heading_${level}_text_color`]"
/>
</template>
</FormGroup>
</template>
<template #preview>
<ABHeading
class="typography-theme-config-block__heading-preview"
:level="level"
>
{{ $t('typographyThemeConfigBlock.headingValue', { i: level }) }}
</ABHeading>
</template>
</ThemeConfigBlockSection>
</template>
</div>
</template>
@ -165,9 +192,12 @@ import themeConfigBlock from '@baserow/modules/builder/mixins/themeConfigBlock'
import ThemeConfigBlockSection from '@baserow/modules/builder/components/theme/ThemeConfigBlockSection'
import ResetButton from '@baserow/modules/builder/components/theme/ResetButton'
import HorizontalAlignmentsSelector from '@baserow/modules/builder/components/HorizontalAlignmentsSelector'
import FontFamilySelector from '@baserow/modules/builder/components/FontFamilySelector'
import PixelValueSelector from '@baserow/modules/builder/components/PixelValueSelector'
const fontSizeMin = 1
const fontSizeMax = 100
const bodyFontSizeMax = 30
const headings = [1, 2, 3, 4, 5, 6]
export default {
@ -176,23 +206,13 @@ export default {
ThemeConfigBlockSection,
ResetButton,
HorizontalAlignmentsSelector,
FontFamilySelector,
PixelValueSelector,
},
mixins: [themeConfigBlock],
data() {
return {
values: {},
allowedValues: [
...headings
.map((level) => [
`heading_${level}_text_color`,
`heading_${level}_font_size`,
`heading_${level}_text_alignment`,
])
.flat(),
'body_font_size',
'body_text_alignment',
'body_text_color',
],
}
},
computed: {
@ -206,12 +226,23 @@ export default {
showBody() {
return !this.extraArgs?.headingLevel
},
showHeadings() {
return !this.extraArgs?.onlyBody
},
fontSizeMin() {
return fontSizeMin
},
fontSizeMax() {
return fontSizeMax
},
bodyFontSizeMax() {
return bodyFontSizeMax
},
},
methods: {
isAllowedKey(key) {
return key.startsWith('heading_') || key.startsWith('body_')
},
},
validations: {
values: {
@ -228,7 +259,7 @@ export default {
required,
integer,
minValue: minValue(fontSizeMin),
maxValue: maxValue(fontSizeMax),
maxValue: maxValue(bodyFontSizeMax),
},
},
},

View file

@ -73,12 +73,18 @@ export class ElementType extends Registerable {
'style_padding_bottom',
'style_padding_left',
'style_padding_right',
'style_margin_top',
'style_margin_bottom',
'style_margin_left',
'style_margin_right',
'style_border_top',
'style_border_bottom',
'style_border_left',
'style_border_right',
'style_background',
'style_background_color',
'style_background_file',
'style_background_mode',
'style_width',
]
}

View file

@ -72,15 +72,22 @@ export const WIDTHS_NEW = {
}
export const BACKGROUND_TYPES = {
NONE: { value: 'none', name: 'backgroundTypes.none' },
COLOR: { value: 'color', name: 'backgroundTypes.color' },
NONE: 'none',
COLOR: 'color',
}
export const BACKGROUND_MODES = {
FILL: 'fill',
TILE: 'tile',
FIT: 'fit',
}
export const WIDTH_TYPES = {
FULL: { value: 'full', name: 'widthTypes.full' },
NORMAL: { value: 'normal', name: 'widthTypes.normal' },
MEDIUM: { value: 'medium', name: 'widthTypes.medium' },
SMALL: { value: 'small', name: 'widthTypes.small' },
MEDIUM: { value: 'medium', name: 'widthTypes.medium' },
NORMAL: { value: 'normal', name: 'widthTypes.normal' },
FULL: { value: 'full', name: 'widthTypes.fullBleed' },
FULL_WIDTH: { value: 'full-width', name: 'widthTypes.fullWidth' },
}
/**

View file

@ -0,0 +1,131 @@
import { Registerable } from '@baserow/modules/core/registry'
export class FontFamilyType extends Registerable {
get name() {
return ''
}
get safeFont() {
return 'sans-serif'
}
}
export class InterFontFamilyType extends FontFamilyType {
static getType() {
return 'inter'
}
get name() {
return 'Inter'
}
}
export class ArialFontFamilyType extends FontFamilyType {
static getType() {
return 'arial'
}
get name() {
return 'Arial'
}
}
export class VerdanaFontFamilyType extends FontFamilyType {
static getType() {
return 'verdana'
}
get name() {
return 'Verdana'
}
}
export class TahomaFontFamilyType extends FontFamilyType {
static getType() {
return 'tahoma'
}
get name() {
return 'Tahoma'
}
}
export class TrebuchetMSFontFamilyType extends FontFamilyType {
static getType() {
return 'trebuchet_ms'
}
get name() {
return 'Trebuchet MS'
}
}
export class TimesNewRomanFontFamilyType extends FontFamilyType {
static getType() {
return 'times_new_roman'
}
get name() {
return 'Times new roman'
}
get safeFont() {
return 'serif'
}
}
export class GeorgiaFontFamilyType extends FontFamilyType {
static getType() {
return 'georgia'
}
get name() {
return 'Georgia'
}
get safeFont() {
return 'serif'
}
}
export class GaramondFontFamilyType extends FontFamilyType {
static getType() {
return 'garamond'
}
get name() {
return 'Garamond'
}
get safeFont() {
return 'serif'
}
}
export class CourierNewFontFamilyType extends FontFamilyType {
static getType() {
return 'courier_new'
}
get name() {
return 'Courier new'
}
get safeFont() {
return 'monospace'
}
}
export class BrushScriptMTFontFamilyType extends FontFamilyType {
static getType() {
return 'brush_script_mt'
}
get name() {
return 'Brush Script MT'
}
get safeFont() {
return 'cursive'
}
}

View file

@ -159,10 +159,6 @@
"imageElement": {
"emptyState": "No alt text defined..."
},
"imageInput": {
"labelDescription": "Default description",
"labelButton": "Upload"
},
"generalForm": {
"labelTitle": "Label",
"labelPlaceholder": "Enter a label (optional)",
@ -300,7 +296,8 @@
"color": "Color"
},
"widthTypes": {
"full": "Full width",
"fullBleed": "Full bleed",
"fullWidth": "Full width",
"normal": "Normal",
"medium": "Medium",
"small": "Small"
@ -364,14 +361,19 @@
"boxRight": "Right",
"backgroundLabel": "Background",
"backgroundColor": "Background color",
"widthLabel": "Width"
"widthLabel": "Width",
"backgroundImage": "Image",
"backgroundImageMode": "Fill mode"
},
"styleBoxForm": {
"borderLabel": "Border",
"paddingLabel": "Padding"
"borderColor": "Border color",
"borderLabel": "Size",
"paddingLabel": "Padding",
"marginLabel": "Margin"
},
"themeConfigBlockType": {
"color": "Colors",
"page": "Page",
"typography": "Typography",
"button": "Button",
"link": "Link",
@ -380,12 +382,23 @@
"colorThemeConfigBlock": {
"primaryColor": "Primary",
"secondaryColor": "Secondary",
"borderColor": "Border"
"borderColor": "Border",
"successColor": "Success",
"warningColor": "Warning",
"errorColor": "Error"
},
"pageThemeConfigBlock": {
"backgroundColor": "Background color",
"backgroundImage": "Background image",
"backgroundMode": "Background mode"
},
"colorThemeConfigBlockType": {
"primary": "Primary",
"secondary": "Secondary",
"border": "Border"
"border": "Border",
"success": "Success",
"warning": "Warning",
"error": "Error"
},
"typographyThemeConfigBlock": {
"headingLabel": "Heading {i} (h{i})",
@ -393,7 +406,8 @@
"color": "Color",
"size": "Size",
"textAlignment": "Alignment",
"bodyLabel": "Body"
"bodyLabel": "Body",
"fontFamily": "Font"
},
"buttonThemeConfigBlock": {
"backgroundColor": "Background color",
@ -402,14 +416,22 @@
"hoverState": "Hover state",
"textAlignment": "Text alignment",
"alignment": "Alignment",
"width": "Width"
"width": "Width",
"textColor": "Text color",
"borderColor": "Border color",
"borderSize": "Border size",
"borderRadius": "Border radius",
"padding": "Padding",
"fontFamily": "Font",
"size": "Font size"
},
"linkThemeConfigBlock": {
"color": "Color",
"link": "Link",
"defaultState": "Default state",
"hoverState": "Hover state",
"alignment": "Alignment"
"alignment": "Alignment",
"fontFamily": "Font"
},
"imageThemeConfigBlock": {
"alignment": "Alignment",
@ -644,5 +666,13 @@
},
"resetButton": {
"reset": "Reset to default theme value"
},
"backgroundModes": {
"fill": "Fill",
"tile": "Tile",
"fit": "Fit"
},
"customStyle": {
"themeOverrides": "Theme overrides"
}
}

View file

@ -124,7 +124,7 @@ export default {
getStyleOverride(key, colorVariables = null) {
return ThemeConfigBlockType.getAllStyles(
this.themeConfigBlocks,
this.element.styles[key] || {},
this.element.styles?.[key] || {},
colorVariables || this.colorVariables,
this.builder.theme
)

View file

@ -62,6 +62,7 @@ export default {
},
getBoxStyleValue(pos) {
return {
margin: this.defaultValues[`style_margin_${pos}`],
padding: this.defaultValues[`style_padding_${pos}`],
border_color: this.defaultValues[`style_border_${pos}_color`],
border_size: this.defaultValues[`style_border_${pos}_size`],
@ -69,6 +70,7 @@ export default {
},
setBoxStyleValue(pos, newValue) {
if (newValue.padding !== undefined) {
this.values[`style_margin_${pos}`] = newValue.margin
this.values[`style_padding_${pos}`] = newValue.padding
this.values[`style_border_${pos}_color`] = newValue.border_color
this.values[`style_border_${pos}_size`] = newValue.border_size
@ -104,7 +106,8 @@ export default {
},
getValuesFromElement(allowedValues) {
return allowedValues.reduce((obj, value) => {
obj[value] = this.element[value] || null
obj[value] =
this.element[value] === undefined ? null : this.element[value]
return obj
}, {})
},

View file

@ -90,6 +90,7 @@ import {
ButtonThemeConfigBlockType,
LinkThemeConfigBlockType,
ImageThemeConfigBlockType,
PageThemeConfigBlockType,
} from '@baserow/modules/builder/themeConfigBlockTypes'
import {
CreateRowWorkflowActionType,
@ -108,6 +109,19 @@ import {
TagsCollectionFieldType,
} from '@baserow/modules/builder/collectionFieldTypes'
import {
InterFontFamilyType,
ArialFontFamilyType,
VerdanaFontFamilyType,
TahomaFontFamilyType,
TrebuchetMSFontFamilyType,
TimesNewRomanFontFamilyType,
GeorgiaFontFamilyType,
GaramondFontFamilyType,
CourierNewFontFamilyType,
BrushScriptMTFontFamilyType,
} from '@baserow/modules/builder/fontFamilyTypes'
export default (context) => {
const { store, app, isDev } = context
@ -146,6 +160,7 @@ export default (context) => {
app.$registry.registerNamespace('pathParamType')
app.$registry.registerNamespace('builderDataProvider')
app.$registry.registerNamespace('themeConfigBlock')
app.$registry.registerNamespace('fontFamily')
app.$registry.register('application', new BuilderApplicationType(context))
app.$registry.register('job', new DuplicatePageJobType(context))
@ -267,6 +282,10 @@ export default (context) => {
'themeConfigBlock',
new ImageThemeConfigBlockType(context)
)
app.$registry.register(
'themeConfigBlock',
new PageThemeConfigBlockType(context)
)
app.$registry.register(
'workflowAction',
@ -313,4 +332,15 @@ export default (context) => {
'collectionField',
new ButtonCollectionFieldType(context)
)
app.$registry.register('fontFamily', new InterFontFamilyType(context))
app.$registry.register('fontFamily', new ArialFontFamilyType(context))
app.$registry.register('fontFamily', new VerdanaFontFamilyType(context))
app.$registry.register('fontFamily', new TahomaFontFamilyType(context))
app.$registry.register('fontFamily', new TrebuchetMSFontFamilyType(context))
app.$registry.register('fontFamily', new TimesNewRomanFontFamilyType(context))
app.$registry.register('fontFamily', new GeorgiaFontFamilyType(context))
app.$registry.register('fontFamily', new GaramondFontFamilyType(context))
app.$registry.register('fontFamily', new CourierNewFontFamilyType(context))
app.$registry.register('fontFamily', new BrushScriptMTFontFamilyType(context))
}

View file

@ -4,10 +4,12 @@ import TypographyThemeConfigBlock from '@baserow/modules/builder/components/them
import ButtonThemeConfigBlock from '@baserow/modules/builder/components/theme/ButtonThemeConfigBlock'
import LinkThemeConfigBlock from '@baserow/modules/builder/components/theme/LinkThemeConfigBlock'
import ImageThemeConfigBlock from '@baserow/modules/builder/components/theme/ImageThemeConfigBlock'
import PageThemeConfigBlock from '@baserow/modules/builder/components/theme/PageThemeConfigBlock'
import { resolveColor } from '@baserow/modules/core/utils/colors'
import {
WIDTHS_NEW,
HORIZONTAL_ALIGNMENTS,
BACKGROUND_MODES,
} from '@baserow/modules/builder/enums'
import get from 'lodash/get'
@ -118,11 +120,7 @@ export class ColorThemeConfigBlockType extends ThemeConfigBlockType {
}
getCSS(theme, colorVariables, baseTheme = null) {
const style = new ThemeStyle()
style.addIfExists(theme, 'primary_color', '--primary-color')
style.addIfExists(theme, 'secondary_color', '--secondary-color')
style.addIfExists(theme, 'border_color', '--border-color')
return style.toObject()
return {}
}
getColorVariables(theme) {
@ -143,6 +141,21 @@ export class ColorThemeConfigBlockType extends ThemeConfigBlockType {
value: 'border',
color: theme.border_color,
},
{
name: i18n.t('colorThemeConfigBlockType.success'),
value: 'success',
color: theme.main_success_color,
},
{
name: i18n.t('colorThemeConfigBlockType.warning'),
value: 'warning',
color: theme.main_warning_color,
},
{
name: i18n.t('colorThemeConfigBlockType.error'),
value: 'error',
color: theme.main_error_color,
},
]
}
@ -185,6 +198,15 @@ export class TypographyThemeConfigBlockType extends ThemeConfigBlockType {
`--heading-h${level}-text-alignment`,
(v) => v
)
style.addIfExists(
theme,
`heading_${level}_font_family`,
`--heading-h${level}-font-family`,
(v) => {
const fontFamilyType = this.app.$registry.get('fontFamily', v)
return `"${fontFamilyType.name}","${fontFamilyType.safeFont}"`
}
)
})
style.addIfExists(
theme,
@ -201,6 +223,10 @@ export class TypographyThemeConfigBlockType extends ThemeConfigBlockType {
`--body-text-alignment`,
(v) => v
)
style.addIfExists(theme, `body_font_family`, `--body-font-family`, (v) => {
const fontFamilyType = this.app.$registry.get('fontFamily', v)
return `"${fontFamilyType.name}","${fontFamilyType.safeFont}"`
})
return style.toObject()
}
@ -233,7 +259,28 @@ export class ButtonThemeConfigBlockType extends ThemeConfigBlockType {
style.addIfExists(
theme,
'button_hover_background_color',
'--hover-button-background-color',
'--button-hover-background-color',
(v) => resolveColor(v, colorVariables)
)
style.addIfExists(theme, 'button_text_color', '--button-text-color', (v) =>
resolveColor(v, colorVariables)
)
style.addIfExists(
theme,
'button_hover_text_color',
'--button-hover-text-color',
(v) => resolveColor(v, colorVariables)
)
style.addIfExists(
theme,
'button_border_color',
'--button-border-color',
(v) => resolveColor(v, colorVariables)
)
style.addIfExists(
theme,
'button_hover_border_color',
'--button-hover-border-color',
(v) => resolveColor(v, colorVariables)
)
style.addIfExists(theme, 'button_width', '--button-width', (v) =>
@ -256,6 +303,51 @@ export class ButtonThemeConfigBlockType extends ThemeConfigBlockType {
[HORIZONTAL_ALIGNMENTS.RIGHT]: 'flex-end',
}[v])
)
style.addIfExists(
theme,
'button_font_alignment',
'--button-text-alignment',
(v) => v
)
style.addIfExists(
theme,
`button_font_family`,
`--button-font-family`,
(v) => {
const fontFamilyType = this.app.$registry.get('fontFamily', v)
return `"${fontFamilyType.name}","${fontFamilyType.safeFont}"`
}
)
style.addIfExists(
theme,
`button_font_size`,
`--button-font-size`,
(v) => `${Math.min(100, v)}px`
)
style.addIfExists(
theme,
`button_border_radius`,
`--button-border-radius`,
(v) => `${v}px`
)
style.addIfExists(
theme,
`button_border_size`,
`--button-border-size`,
(v) => `${v}px`
)
style.addIfExists(
theme,
`button_horizontal_padding`,
`--button-horizontal-padding`,
(v) => `${v}px`
)
style.addIfExists(
theme,
`button_vertical_padding`,
`--button-vertical-padding`,
(v) => `${v}px`
)
return style.toObject()
}
@ -299,6 +391,10 @@ export class LinkThemeConfigBlockType extends ThemeConfigBlockType {
[HORIZONTAL_ALIGNMENTS.RIGHT]: 'flex-end',
}[v])
)
style.addIfExists(theme, `link_font_family`, `--link-font-family`, (v) => {
const fontFamilyType = this.app.$registry.get('fontFamily', v)
return `"${fontFamilyType.name}","${fontFamilyType.safeFont}"`
})
return style.toObject()
}
@ -350,31 +446,40 @@ export class ImageThemeConfigBlockType extends ThemeConfigBlockType {
baseTheme?.image_constraint
)
style.style['--image-wrapper-width'] = `${imageMaxWidth}%`
style.style['--image-wrapper-max-width'] = `${imageMaxWidth}%`
if (imageMaxHeight) {
style.style['--image-max-width'] = 'auto'
style.style['--image-wrapper-max-height'] = `${imageMaxHeight}px`
if (Object.prototype.hasOwnProperty.call(theme, 'image_max_width')) {
style.style['--image-wrapper-width'] = `${imageMaxWidth}%`
style.style['--image-wrapper-max-width'] = `${imageMaxWidth}%`
}
switch (imageConstraint) {
case 'cover':
style.style['--image-wrapper-width'] = '100%'
style.style['--image-object-fit'] = 'cover'
style.style['--image-width'] = '100%'
style.style['--image-height'] = '100%'
break
case 'contain':
style.style['--image-object-fit'] = 'contain'
style.style['--image-max-width'] = '100%'
break
case 'full-width':
style.style['--image-object-fit'] = 'fill'
style.style['--image-width'] = '100%'
style.style['--image-height'] = '100%'
style.style['--image-max-width'] = 'none'
break
if (Object.prototype.hasOwnProperty.call(theme, 'image_max_height')) {
if (imageMaxHeight) {
style.style['--image-max-width'] = 'auto'
style.style['--image-wrapper-max-height'] = `${imageMaxHeight}px`
} else {
style.style['--image-max-width'] = 'auto'
style.style['--image-wrapper-max-height'] = 'auto'
}
}
if (Object.prototype.hasOwnProperty.call(theme, 'image_constraint')) {
switch (imageConstraint) {
case 'cover':
style.style['--image-wrapper-width'] = '100%'
style.style['--image-object-fit'] = 'cover'
style.style['--image-width'] = '100%'
style.style['--image-height'] = '100%'
break
case 'contain':
style.style['--image-object-fit'] = 'contain'
style.style['--image-max-width'] = '100%'
break
case 'full-width':
style.style['--image-object-fit'] = 'fill'
style.style['--image-width'] = '100%'
style.style['--image-height'] = '100%'
style.style['--image-max-width'] = 'none'
break
}
}
return style.toObject()
@ -388,3 +493,50 @@ export class ImageThemeConfigBlockType extends ThemeConfigBlockType {
return 60
}
}
export class PageThemeConfigBlockType extends ThemeConfigBlockType {
static getType() {
return 'page'
}
get label() {
return this.app.i18n.t('themeConfigBlockType.page')
}
getCSS(theme, colorVariables, baseTheme = null) {
const style = new ThemeStyle()
style.addIfExists(
theme,
'page_background_color',
'--page-background-color',
(v) => resolveColor(v, colorVariables)
)
style.addIfExists(
theme,
'page_background_file',
'--page-background-image',
(v) => (v ? `url(${v.url})` : 'none')
)
if (theme.page_background_mode === BACKGROUND_MODES.FILL) {
style.style['--page-background-size'] = 'cover'
style.style['--page-background-repeat'] = 'no-repeat'
}
if (theme.page_background_mode === BACKGROUND_MODES.TILE) {
style.style['--page-background-size'] = 'auto'
style.style['--page-background-repeat'] = 'repeat'
}
if (theme.page_background_mode === BACKGROUND_MODES.FIT) {
style.style['--page-background-size'] = 'contain'
style.style['--page-background-repeat'] = 'no-repeat'
}
return style.toObject()
}
get component() {
return PageThemeConfigBlock
}
getOrder() {
return 15
}
}

View file

@ -31,3 +31,5 @@
@import 'update_user_source_form';
@import 'user_source_users_context';
@import 'device_selector';
@import 'padding_selector';
@import 'page';

View file

@ -1,12 +1,19 @@
.element__wrapper {
background-color: var(--background-color, $black);
border-top: var(--border-top, none);
border-bottom: var(--border-bottom, none);
border-left: var(--border-left, none);
border-right: var(--border-right, none);
background-color: var(--element-background-color, transparent);
background-image: var(--element-background-image, none);
background-size: var(--element-background-size, cover);
background-repeat: var(--element-background-repeat, no-repeat);
margin: 0 auto;
max-width: $builder-page-max-width;
// We use padding here as margin to prevent margin collapsing
padding: var(--element-margin-top, 0) var(--element-margin-right, 0)
var(--element-margin-bottom, 0) var(--element-margin-left, 0);
&--full-bleed {
max-width: 100%;
}
&--full-width {
max-width: 100%;
}
@ -21,22 +28,18 @@
}
.element__inner-wrapper {
padding: var(--padding-top, 0) var(--padding-right, 0)
var(--padding-bottom, 0) var(--padding-left, 0);
border-top: var(--element-border-top, none);
border-bottom: var(--element-border-bottom, none);
border-left: var(--element-border-left, none);
border-right: var(--element-border-right, none);
padding: var(--element-padding-top, 0) var(--element-padding-right, 0)
var(--element-padding-bottom, 0) var(--element-padding-left, 0);
margin: 0 auto;
max-width: $builder-page-max-width;
&--full-width {
.element__wrapper--full-width & {
max-width: 100%;
}
&--medium-width {
max-width: 960px;
}
&--small-width {
max-width: 680px;
}
}
.element {

View file

@ -1,52 +1,26 @@
.ab-button {
cursor: pointer;
display: inline-block;
color: $white;
background-color: var(--button-background-color, $black);
line-height: 28px;
border: none;
white-space: nowrap;
display: inline-block;
text-decoration: none;
line-height: 1em;
color: var(--button-text-color, $white);
background-color: var(--button-background-color, $black);
font-size: var(--button-font-size, 12px);
border: var(--button-border-size, 0) solid var(--button-border-color, black);
border-radius: var(--button-border-radius, 4px);
width: var(--button-width, auto);
text-align: var(--button-text-alignment, center);
align-self: var(--button-alignment, flex-start);
@include rounded($rounded);
&--full-width {
width: 100%;
}
&--left {
text-align: left;
}
&--center {
text-align: center;
}
&--right {
text-align: right;
}
&--small {
font-size: 12px;
padding: 0 8px;
}
&--medium {
font-size: 14px;
padding: 0 12px;
}
&--large {
font-size: 15px;
padding: 5px 12px;
}
font-family: var(--button-font-family, Inter);
padding: var(--button-vertical-padding, 4px)
var(--button-horizontal-padding, 12px);
&:not(.loading-spinner):hover,
&:not(.loading-spinner).ab-button--force-hover {
background-color: var(--hover-button-background-color, $black);
background-color: var(--button-hover-background-color, $black);
border-color: var(--button-hover-border-color, $white);
color: var(--button-hover-text-color, $white);
text-decoration: none;
}

View file

@ -6,35 +6,41 @@
color: var(--heading-h1-color, $black);
font-size: var(--heading-h1-font-size, 30px);
text-align: var(--heading-h1-text-alignment, left);
font-family: var(--heading-h1-font-family, Inter);
}
.ab-heading--h2 {
color: var(--heading-h2-color, $black);
font-size: var(--heading-h2-font-size, 26px);
text-align: var(--heading-h2-text-alignment, left);
font-family: var(--heading-h2-font-family, Inter);
}
.ab-heading--h3 {
color: var(--heading-h3-color, $black);
font-size: var(--heading-h3-font-size, 22px);
text-align: var(--heading-h3-text-alignment, left);
font-family: var(--heading-h3-font-family, Inter);
}
.ab-heading--h4 {
color: var(--heading-h4-color, $black);
font-size: var(--heading-h4-font-size, 18px);
text-align: var(--heading-h4-text-alignment, left);
font-family: var(--heading-h4-font-family, Inter);
}
.ab-heading--h5 {
color: var(--heading-h5-color, $black);
font-size: var(--heading-h5-font-size, 14px);
text-align: var(--heading-h5-text-alignment, left);
font-family: var(--heading-h5-font-family, Inter);
}
.ab-heading--h6 {
color: var(--heading-h6-color, $black);
font-size: var(--heading-h6-font-size, 14px);
text-align: var(--heading-h6-text-alignment, left);
font-family: var(--heading-h6-font-family, Inter);
font-style: italic;
}

View file

@ -3,6 +3,7 @@
text-decoration: var(--link-text-decoration, underline);
color: var(--link-text-color, $black);
align-self: var(--link-text-alignment, flex-start);
font-family: var(--link-font-family, Inter);
&:hover,
&--force-hover {

View file

@ -3,4 +3,10 @@
margin: 0;
color: var(--body-text-color, $black);
text-align: var(--body-text-alignment, left);
font-family: var(--body-font-family, Inter);
// for markdown text
strong {
color: var(--body-text-color, $black);
}
}

View file

@ -0,0 +1,8 @@
.padding-selector {
display: flex;
gap: 5px;
}
.padding-selector__input {
flex: 1;
}

View file

@ -0,0 +1,11 @@
.page {
background-color: var(--page-background-color, #fff);
background-image: var(--page-background-image, none);
background-size: var(--page-background-size, cover);
background-repeat: var(--page-background-repeat, no-repeat);
.public-page & {
// We want to fill the screen when it's the published version
min-height: 100vh;
}
}

View file

@ -35,10 +35,19 @@
.page-preview__scaled {
width: 100%;
min-height: 100%;
background-color: $white;
overflow: auto;
position: relative;
// These properties are duplicated from the page element because during the SSR
// the screen size is fixed to an arbitrary value and when returned to the browser
// there is a small gap at the bottom of the screen between the end of the `.page` and
// the end of this element. By duplicating them, we hide the difference before the
// front hydratation.
background-color: var(--page-background-color, #fff);
background-image: var(--page-background-image, none);
background-size: var(--page-background-size, cover);
background-repeat: var(--page-background-repeat, no-repeat);
// We need to do this because the border of the preview is round and we need to round
// the border of the selection box so that border has to have the same shape.
> div:first-child {

View file

@ -6,6 +6,7 @@
place-content: center space-between;
margin-bottom: 15px;
width: 60%;
padding: 0;
user-select: none;
&:hover {
@ -24,6 +25,25 @@
.theme-config-block {
position: relative;
& .control--horizontal {
flex-wrap: nowrap;
.control__label {
flex-basis: 40%;
color: $color-neutral-700;
}
.control__elements {
flex-basis: 60%;
white-space: initial;
}
.control__wrapper {
flex-basis: 60%;
white-space: initial;
}
}
&:not(.theme-config-block--no-preview)::after {
@include absolute(0, calc(40% - 14px), 0, auto);

View file

@ -2,6 +2,7 @@
display: flex;
width: 100%;
gap: 28px;
margin-bottom: 15px;
}
.theme-config-block-section__properties {
@ -17,6 +18,8 @@
display: flex;
flex-direction: column;
justify-content: flex-start;
max-width: calc(40% - 15px);
overflow: hidden;
.theme-config-block--no-preview & {
display: none;

View file

@ -1,3 +1,7 @@
.typography-theme-config-block__input-number {
width: 80px;
}
.typography-theme-config-block__heading-preview {
@extend %ellipsis;
}

View file

@ -3,9 +3,30 @@
width: 16px;
height: 16px;
border-radius: 2px;
position: relative;
border: 1px solid $palette-neutral-400;
}
.color-input__preview::before {
content: '';
position: absolute;
inset: 0;
background-size: 14px 14px;
background-image: conic-gradient(
$white 90deg,
$color-neutral-400 90deg 180deg,
$white 180deg 270deg,
$color-neutral-400 270deg
);
}
.color-input__preview::after {
content: '';
position: absolute;
inset: 0;
background-color: var(--selected-color, black);
}
.color-input__input {
width: 100%;
border: 1px solid $palette-neutral-400;

View file

@ -1,11 +1,12 @@
.color-picker {
display: flex;
gap: 22px;
}
.color-picker__space {
position: relative;
flex: 230px 0 0;
height: 230px;
width: 230px;
overflow: hidden;
border: 1px solid $color-neutral-200;
background-image: linear-gradient(to top, #000, transparent),
@ -38,8 +39,7 @@
.color-picker__slider {
position: relative;
flex: 18px 0 0;
margin-left: 22px;
width: 18px;
overflow: hidden;
border: 1px solid $color-neutral-200;

View file

@ -1,11 +1,12 @@
.color-picker-context {
padding: 15px;
padding: 20px;
display: flex;
flex-direction: column;
}
.color-picker-context__color {
display: flex;
margin-top: 12px;
width: 310px;
}
.color-picker-context__color-type {
@ -13,25 +14,28 @@
}
.color-picker-context__color-hex {
flex-basis: 95px;
width: 95px;
margin-left: 16px;
}
.color-picker-context__color-rgb {
display: flex;
gap: 5px;
margin: 0 5px;
width: 135px;
margin-left: 11px;
// input {
// width: 38px;
// padding-left: 5px;
// padding-right: 5px;
// }
.form-input__input {
padding: 12px 5px;
text-align: center;
}
}
.color-picker-context__color-opacity {
flex: 0 0 72px;
margin-left: auto;
width: 80px;
.form-input__input {
padding-right: 5px;
}
}
.color-picker-context__variables {

View file

@ -17,6 +17,11 @@
align-items: start;
}
}
&.control--error .form-input__input,
&.control--error .form-input__icon {
color: $palette-red-600;
}
}
.control__elements--flex {
@ -180,3 +185,26 @@
@include flex-align-items(3px);
}
.form-section {
border-bottom: 1px solid $color-neutral-200;
padding-bottom: 15px;
margin-bottom: 15px;
// If we have an horizontal FormGroup
// these labels should be grayer for better effect.
& .control--horizontal .control__label {
color: $color-neutral-700;
}
&:last-child {
margin-bottom: 0;
padding-bottom: 0;
border: none;
}
}
.form-section__title {
font-size: 13px;
font-weight: 500;
}

View file

@ -28,7 +28,7 @@
}
&.form-input--error {
border-color: $palette-red-600;
color: $palette-red-600;
}
&.form-input--disabled {

View file

@ -1,6 +1,6 @@
.image-input {
display: flex;
align-items: flex-start;
align-items: center;
gap: 18px;
}
@ -9,11 +9,13 @@
}
.image-input__image-placeholder-img {
object-fit: contain;
width: 60px;
height: 60px;
}
.image-input__image-upload {
flex: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
@ -21,30 +23,7 @@
}
.image-input__image-upload-description {
font-weight: lighter;
color: $palette-neutral-900;
}
.image-input__thumbnail-remove {
position: absolute;
inset: 0;
display: none;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 0.1);
color: $color-error-500;
font-size: 12px;
font-weight: 600;
&:hover {
text-decoration: none;
}
:hover > & {
display: flex;
}
i {
margin-right: 4px;
}
margin-bottom: 0;
line-height: 1em;
}

View file

@ -4,6 +4,7 @@
ref="colorPicker"
:value="value"
:variables="localColorVariables"
:allow-opacity="allowOpacity"
@input="$emit('input', $event)"
/>
<div
@ -15,7 +16,7 @@
<span
class="color-input__preview"
:style="{
'background-color': actualValue,
'--selected-color': actualValue,
}"
/>
<span>{{ displayValue }}</span>
@ -51,6 +52,11 @@ export default {
required: false,
default: null,
},
allowOpacity: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
variablesMap() {

View file

@ -46,6 +46,7 @@
/>
</div>
<div
v-if="allowOpacity"
ref="alphaSpace"
class="color-picker__slider color-picker__slider--alpha color-picker__thumb--negative-bottom-margin"
draggable="false"
@ -91,6 +92,11 @@ export default {
type: String,
default: '#ffffffff',
},
allowOpacity: {
type: Boolean,
required: false,
default: true,
},
},
data() {
return {

View file

@ -2,6 +2,7 @@
<Context class="color-picker-context">
<ColorPicker
:value="hexColorIncludingAlpha"
:allow-opacity="allowOpacity"
@input="setColorFromPicker($event)"
></ColorPicker>
<div class="color-picker-context__color">
@ -9,21 +10,18 @@
v-model="type"
class="dropdown--floating color-picker-context__color-type"
:show-search="false"
small
>
<DropdownItem name="Hex" :value="COLOR_NOTATIONS.HEX"></DropdownItem>
<DropdownItem name="RGB" :value="COLOR_NOTATIONS.RGB"></DropdownItem>
</Dropdown>
<div v-if="type === 'hex'" class="color-picker-context__color-hex">
<FormInput
size="large"
:value="hexColorExcludingAlpha"
@input="hexChanged"
/>
<FormInput small :value="hexColorExcludingAlpha" @input="hexChanged" />
</div>
<div v-if="type === 'rgb'" class="color-picker-context__color-rgb">
<FormInput
type="number"
size="large"
small
:min="0"
:max="255"
:value="r"
@ -32,7 +30,7 @@
/>
<FormInput
type="number"
size="large"
small
:min="0"
:max="255"
:value="g"
@ -41,7 +39,7 @@
/>
<FormInput
type="number"
size="large"
small
:min="0"
:max="255"
:value="b"
@ -49,17 +47,18 @@
@input="rgbaChanged($event, 'b')"
/>
</div>
<div class="color-picker-context__color-opacity">
<div class="flex-grow-1" />
<div v-if="allowOpacity" class="color-picker-context__color-opacity">
<FormInput
type="number"
size="large"
small
:min="0"
:max="100"
:value="a"
remove-number-input-controls
icon-right="iconoir-percentage"
remove-number-input-controls
@input="rgbaChanged($event, 'a')"
></FormInput>
/>
</div>
</div>
<div
@ -121,6 +120,11 @@ export default {
required: false,
default: () => [],
},
allowOpacity: {
type: Boolean,
required: false,
default: true,
},
},
data() {
return {
@ -182,7 +186,12 @@ export default {
this.r = rgba.r * 255
this.g = rgba.g * 255
this.b = rgba.b * 255
this.a = Math.round(rgba.a * 100)
if (this.allowOpacity) {
this.a = Math.round(rgba.a * 100)
} else {
this.a = 100
}
this.hexColorIncludingAlpha = convertRgbToHex(rgba)

View file

@ -7,6 +7,7 @@
'control--horizontal-variable': horizontalVariable,
'control--messages': hasMessages,
'control--after-input': hasAfterInputSlot,
'control--error': hasError,
}"
>
<label
@ -42,7 +43,8 @@
<slot v-if="hasHelperSlot" name="helper" />
</p>
<p v-if="hasError" class="control__messages--error">
<slot name="error" />
<slot v-if="hasErrorSlot" name="error" />
<template v-else-if="errorMessage">{{ errorMessage }}</template>
</p>
<p v-if="hasWarningSlot" class="control__messages--warning">
<slot name="warning" />
@ -64,6 +66,14 @@ export default {
required: false,
default: false,
},
/**
* Shorthand when you don't need a specific error display.
*/
errorMessage: {
type: String,
required: false,
default: '',
},
/**
* The id of the form group.
*/
@ -123,7 +133,10 @@ export default {
},
computed: {
hasError() {
return Boolean(this.error)
return Boolean(this.error) || Boolean(this.errorMessage)
},
hasErrorSlot() {
return !!this.$slots.error
},
hasLabelSlot() {
return !!this.$slots.label

View file

@ -0,0 +1,19 @@
<template>
<div class="form-section">
<h3 v-if="title" class="form-section__title">{{ title }}</h3>
<slot />
</div>
</template>
<script>
export default {
name: 'FormSection',
props: {
title: {
type: String,
required: false,
default: null,
},
},
}
</script>

View file

@ -1,21 +1,13 @@
<template>
<div class="image-input">
<div class="image-input__image-placeholder">
<div class="image-input image-input--with-image">
<div v-if="imageUrl" class="image-input__image-placeholder">
<img class="image-input__image-placeholder-img" :src="imageUrl" />
<a
v-if="removable"
class="image-input__thumbnail-remove"
@click="$emit('input', null)"
>
<i class="iconoir-cancel"></i>
{{ $t('action.remove') }}
</a>
</div>
<div>
<div class="image-input__image-upload">
<span class="image-input__image-upload-description">
<div class="image-input__image-upload">
<template v-if="!hasImage">
<p class="image-input__image-upload-description">
{{ labelDescription || $t('imageInput.labelDescription') }}
</span>
</p>
<Button
icon="iconoir-upload-square"
type="upload"
@ -23,7 +15,14 @@
>
{{ labelButton || $t('imageInput.labelButton') }}
</Button>
</div>
</template>
</div>
<div class="image-input__image-delete">
<ButtonIcon
v-if="hasImage"
icon="iconoir-bin"
@click="$emit('input', null)"
/>
</div>
<UserFilesModal
ref="userFilesModal"
@ -75,7 +74,7 @@ export default {
defaultImage: {
type: String,
required: false,
default: '',
default: null,
},
},
data() {
@ -83,14 +82,21 @@ export default {
},
computed: {
imageUrl() {
if (!this.value) {
return this.defaultImage
if (this.value === null) {
if (this.defaultImage) {
return this.defaultImage
} else {
return null
}
}
return this.value.url
},
removable() {
return this.value !== null
},
hasImage() {
return this.value !== null
},
},
methods: {
openFileUploadModal() {

View file

@ -1,4 +1,13 @@
export const IMAGE_FILE_TYPES = ['image/jpeg', 'image/jpg', 'image/png']
export const IMAGE_FILE_TYPES = [
'image/jpeg',
'image/jpg',
'image/png',
'image/apng',
'image/gif',
'image/tiff',
'image/bmp',
'image/webp',
]
export const FAVICON_IMAGE_FILE_TYPES = [...IMAGE_FILE_TYPES, 'image/x-icon']

View file

@ -711,5 +711,9 @@
},
"colorInput": {
"default": "Default"
},
"imageInput": {
"labelDescription": "Select an image to upload...",
"labelButton": "Upload"
}
}

View file

@ -38,17 +38,23 @@ export default {
},
methods: {
/**
* Returns all the provided default values, but if the allowedValues are set
* an object only containing those values is returned. This could be useful
* when the defaultValues also contain other values which must not be used
* when submitting.
* Returns whether a key of the given defaultValue should be handled by this
* form component. This is useful when the defaultValues also contain other
* values which must not be used when submitting. By default this implementation
* is filtered by the list of `allowedValues`.
*/
isAllowedKey(key) {
if (this.allowedValues !== null) {
return this.allowedValues.includes(key)
}
return true
},
/**
* Returns all the provided default values filtered by the `isAllowedKey` method.
*/
getDefaultValues() {
if (this.allowedValues === null) {
return clone(this.defaultValues)
}
return Object.keys(this.defaultValues).reduce((result, key) => {
if (this.allowedValues.includes(key)) {
if (this.isAllowedKey(key)) {
let value = this.defaultValues[key]
// If the value is an array or object, it could be that it contains

View file

@ -612,7 +612,7 @@
</template>
</Alert>
<Alert type="error" close-button>
<Alert type="danger" close-button>
<template #title>Alert title</template>
<template #actions>
<button
@ -1375,7 +1375,7 @@
toggle error toast
</Button>
<Button
type="warning"
type="danger"
@click="
$store.dispatch('toast/warning', {
title: 'Custom warning toast',
@ -1957,6 +1957,11 @@
{{ color }} - {{ resolveColor(color, colorVariables) }} <br /><br />
<br />
<ColorInput v-model="color" :color-variables="colorVariables" />
<ColorInput
v-model="color"
:color-variables="colorVariables"
:allow-opacity="false"
/>
</div>
<div class="margin-bottom-3">

View file

@ -38,6 +38,7 @@ import FormGroup from '@baserow/modules/core/components/FormGroup'
import FormRow from '@baserow/modules/core/components/FormRow'
import Logo from '@baserow/modules/core/components/Logo'
import ReadOnlyForm from '@baserow/modules/core/components/ReadOnlyForm'
import FormSection from '@baserow/modules/core/components/FormSection'
import lowercase from '@baserow/modules/core/filters/lowercase'
import uppercase from '@baserow/modules/core/filters/uppercase'
@ -105,6 +106,7 @@ function setupVue(Vue) {
Vue.component('SelectSearch', SelectSearch)
Vue.component('Logo', Logo)
Vue.component('ReadOnlyForm', ReadOnlyForm)
Vue.component('FormSection', FormSection)
Vue.filter('lowercase', lowercase)
Vue.filter('uppercase', uppercase)