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

Improve AB styling capabilities v1

This commit is contained in:
Jérémie Pardou 2024-07-03 12:05:50 +00:00
parent c352726f0a
commit 495af48ede
73 changed files with 2632 additions and 1344 deletions
backend
changelog/entries/unreleased/feature
web-frontend

View file

@ -107,6 +107,7 @@ class PublicElementSerializer(serializers.ModelSerializer):
"parent_element_id",
"place_in_container",
"visibility",
"styles",
"style_border_top_color",
"style_border_top_size",
"style_padding_top",

View file

@ -52,6 +52,7 @@ class ElementSerializer(serializers.ModelSerializer):
"parent_element_id",
"place_in_container",
"visibility",
"styles",
"style_border_top_color",
"style_border_top_size",
"style_padding_top",
@ -136,6 +137,7 @@ class UpdateElementSerializer(serializers.ModelSerializer):
model = Element
fields = (
"visibility",
"styles",
"style_border_top_color",
"style_border_top_size",
"style_padding_top",

View file

@ -235,9 +235,15 @@ class BuilderConfig(AppConfig):
operation_type_registry.register(UpdateThemeOperationType())
from .theme.registries import theme_config_block_registry
from .theme.theme_config_block_types import MainThemeConfigBlockType
from .theme.theme_config_block_types import (
ButtonThemeConfigBlockType,
ColorThemeConfigBlockType,
TypographyThemeConfigBlockType,
)
theme_config_block_registry.register(MainThemeConfigBlockType())
theme_config_block_registry.register(ColorThemeConfigBlockType())
theme_config_block_registry.register(TypographyThemeConfigBlockType())
theme_config_block_registry.register(ButtonThemeConfigBlockType())
from .workflow_actions.registries import builder_workflow_action_type_registry
from .workflow_actions.workflow_action_types import (

View file

@ -35,6 +35,7 @@ class ElementHandler:
allowed_fields_create = [
"parent_element_id",
"place_in_container",
"styles",
"visibility",
"style_border_top_color",
"style_border_top_size",
@ -57,6 +58,7 @@ class ElementHandler:
"parent_element_id",
"place_in_container",
"visibility",
"styles",
"style_border_top_color",
"style_border_top_size",
"style_padding_top",

View file

@ -149,6 +149,12 @@ class Element(
db_index=True,
)
styles = models.JSONField(
default=dict,
help_text="The theme overrides for this element",
null=True, # TODO zdm remove me in next release
)
style_border_top_color = models.CharField(
max_length=20,
default="border",
@ -387,6 +393,8 @@ class HeadingElement(Element):
level = models.IntegerField(
choices=HeadingLevel.choices, default=1, help_text="The level of the heading"
)
# TODO zdm remove me in next release
font_color = models.CharField(
max_length=20,
default="default",
@ -500,6 +508,7 @@ class LinkElement(Element, NavigationElementMixin):
max_length=10,
default=HorizontalAlignments.LEFT,
)
# TODO zdm remove me in next release
button_color = models.CharField(
max_length=20,
default="primary",
@ -585,6 +594,7 @@ class FormContainerElement(ContainerElement):
"values after a successful form submission.",
)
# TODO zdm remove me in next release
button_color = models.CharField(
max_length=20,
default="primary",
@ -721,6 +731,8 @@ class ButtonElement(Element):
max_length=10,
default=HorizontalAlignments.LEFT,
)
# TODO zdm remove me in next release
button_color = models.CharField(
max_length=20,
default="primary",
@ -789,6 +801,7 @@ class TableElement(CollectionElement):
A table element
"""
# TODO zdm remove me in next release
button_color = models.CharField(
max_length=20,
default="primary",

View file

@ -0,0 +1,235 @@
# Generated by Django 4.1.13 on 2024-05-31 12:52
import django.db.models.deletion
from django.db import migrations, models
import baserow.core.fields
from baserow.core.utils import lighten_color
def migrate_config_blocks(apps, schema_editor):
MainThemeConfigBlock = apps.get_model("builder", "mainthemeconfigblock")
ColorThemeConfigBlock = apps.get_model("builder", "colorthemeconfigblock")
TypographyThemeConfigBlock = apps.get_model("builder", "typographythemeconfigblock")
ButtonThemeConfigBlock = apps.get_model("builder", "buttonthemeconfigblock")
for main in MainThemeConfigBlock.objects.all():
ColorThemeConfigBlock.objects.create(
builder=main.builder,
primary_color=main.primary_color,
secondary_color=main.secondary_color,
border_color=main.border_color,
)
TypographyThemeConfigBlock.objects.create(
builder=main.builder,
heading_1_font_size=main.heading_1_font_size,
heading_1_text_color=main.heading_1_color,
heading_2_font_size=main.heading_2_font_size,
heading_2_text_color=main.heading_2_color,
heading_3_font_size=main.heading_3_font_size,
heading_3_text_color=main.heading_3_color,
)
ButtonThemeConfigBlock.objects.create(builder=main.builder)
def migrate_element_styles(apps, schema_editor):
"""
Migrates on model element styles into the style property.
"""
Element = apps.get_model("builder", "element")
Element.objects.update(styles={})
ButtonElement = apps.get_model("builder", "buttonelement")
HeadingElement = apps.get_model("builder", "headingelement")
LinkElement = apps.get_model("builder", "linkelement")
FormContainerElement = apps.get_model("builder", "formcontainerelement")
TableElement = apps.get_model("builder", "tableelement")
elements_to_update = []
for heading in HeadingElement.objects.all():
heading.styles["typography"] = {
f"heading_{heading.level}_text_color": heading.font_color
}
elements_to_update.append(heading)
HeadingElement.objects.bulk_update(elements_to_update, ["styles"], batch_size=100)
elements_to_update = []
for button in ButtonElement.objects.all():
if button.button_color != "primary":
button.styles["button"] = {"button_background_color": button.button_color}
button.styles["button"]["button_hover_background_color"] = lighten_color(
button.button_color, 0.3
)
elements_to_update.append(button)
ButtonElement.objects.bulk_update(elements_to_update, ["styles"], batch_size=100)
elements_to_update = []
for element in LinkElement.objects.all():
if element.button_color != "primary":
element.styles["button"] = {"button_background_color": element.button_color}
element.styles["button"]["button_hover_background_color"] = lighten_color(
element.button_color, 0.3
)
elements_to_update.append(element)
LinkElement.objects.bulk_update(elements_to_update, ["styles"], batch_size=100)
elements_to_update = []
for element in FormContainerElement.objects.all():
if element.button_color != "primary":
element.styles["button"] = {"button_background_color": element.button_color}
element.styles["button"]["button_hover_background_color"] = lighten_color(
element.button_color, 0.3
)
elements_to_update.append(element)
FormContainerElement.objects.bulk_update(
elements_to_update, ["styles"], batch_size=100
)
elements_to_update = []
for element in TableElement.objects.all():
if element.button_color != "primary":
element.styles["button"] = {"button_background_color": element.button_color}
element.styles["button"]["button_hover_background_color"] = lighten_color(
element.button_color, 0.3
)
elements_to_update.append(element)
TableElement.objects.bulk_update(elements_to_update, ["styles"], batch_size=100)
class Migration(migrations.Migration):
dependencies = [
("builder", "0024_element_role_type_element_roles"),
]
operations = [
migrations.CreateModel(
name="ButtonThemeConfigBlock",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"button_background_color",
models.CharField(
blank=True,
default="primary",
help_text="The background color of buttons",
max_length=20,
),
),
(
"button_hover_background_color",
models.CharField(
blank=True,
default="#96baf6ff",
help_text="The background color of buttons when hovered",
max_length=20,
),
),
(
"builder",
baserow.core.fields.AutoOneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)s",
to="builder.builder",
),
),
],
options={
"abstract": False,
},
),
migrations.CreateModel(
name="ColorThemeConfigBlock",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("primary_color", models.CharField(default="#5190efff", max_length=9)),
(
"secondary_color",
models.CharField(default="#0eaa42ff", max_length=9),
),
("border_color", models.CharField(default="#d7d8d9ff", max_length=9)),
(
"builder",
baserow.core.fields.AutoOneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)s",
to="builder.builder",
),
),
],
options={
"abstract": False,
},
),
migrations.CreateModel(
name="TypographyThemeConfigBlock",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("heading_1_font_size", models.SmallIntegerField(default=24)),
(
"heading_1_text_color",
models.CharField(default="#070810ff", max_length=9),
),
("heading_2_font_size", models.SmallIntegerField(default=20)),
(
"heading_2_text_color",
models.CharField(default="#070810ff", max_length=9),
),
("heading_3_font_size", models.SmallIntegerField(default=16)),
(
"heading_3_text_color",
models.CharField(default="#070810ff", max_length=9),
),
(
"builder",
baserow.core.fields.AutoOneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)s",
to="builder.builder",
),
),
],
options={
"abstract": False,
},
),
migrations.AddField(
model_name="element",
name="styles",
field=models.JSONField(
default=dict,
help_text="The theme overrides for this element",
null=True,
),
),
migrations.RunPython(
migrate_config_blocks, reverse_code=migrations.RunPython.noop
),
migrations.RunPython(
migrate_element_styles, reverse_code=migrations.RunPython.noop
),
]

View file

@ -3,7 +3,11 @@ from django.db import models
from baserow.contrib.builder.domains.models import Domain, PublishDomainJob
from baserow.contrib.builder.elements.models import Element
from baserow.contrib.builder.pages.models import Page
from baserow.contrib.builder.theme.models import MainThemeConfigBlock
from baserow.contrib.builder.theme.models import (
ButtonThemeConfigBlock,
ColorThemeConfigBlock,
TypographyThemeConfigBlock,
)
from baserow.core.models import Application, UserFile
__all__ = [
@ -12,7 +16,9 @@ __all__ = [
"Domain",
"PublishDomainJob",
"Element",
"MainThemeConfigBlock",
"ColorThemeConfigBlock",
"TypographyThemeConfigBlock",
"ButtonThemeConfigBlock",
]

View file

@ -15,14 +15,43 @@ class ThemeConfigBlock(models.Model):
class MainThemeConfigBlock(ThemeConfigBlock):
# colors
# TODO zdm remove the whole model in next release
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")
# headings
heading_1_font_size = models.SmallIntegerField(default=24)
heading_1_color = models.CharField(max_length=9, default="#070810ff")
heading_2_font_size = models.SmallIntegerField(default=20)
heading_2_color = models.CharField(max_length=9, default="#070810ff")
heading_3_font_size = models.SmallIntegerField(default=16)
heading_3_color = models.CharField(max_length=9, default="#070810ff")
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")
class TypographyThemeConfigBlock(ThemeConfigBlock):
heading_1_font_size = models.SmallIntegerField(default=24)
heading_1_text_color = models.CharField(max_length=9, default="#070810ff")
heading_2_font_size = models.SmallIntegerField(default=20)
heading_2_text_color = models.CharField(max_length=9, default="#070810ff")
heading_3_font_size = models.SmallIntegerField(default=16)
heading_3_text_color = models.CharField(max_length=9, default="#070810ff")
class ButtonThemeConfigBlock(ThemeConfigBlock):
button_background_color = models.CharField(
max_length=20,
default="primary",
blank=True,
help_text="The background color of buttons",
)
button_hover_background_color = models.CharField(
max_length=20,
default="#96baf6ff",
blank=True,
help_text="The background color of buttons when hovered",
)

View file

@ -1,21 +1,62 @@
from .models import MainThemeConfigBlock
from .models import (
ButtonThemeConfigBlock,
ColorThemeConfigBlock,
TypographyThemeConfigBlock,
)
from .registries import ThemeConfigBlockType
main_theme_config_block_fields = [
"primary_color",
"secondary_color",
"border_color",
"heading_1_font_size",
"heading_1_color",
"heading_2_font_size",
"heading_2_color",
"heading_3_font_size",
"heading_3_color",
]
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 MainThemeConfigBlockType(ThemeConfigBlockType):
type = "main"
model_class = MainThemeConfigBlock
allowed_fields = main_theme_config_block_fields
serializer_field_names = main_theme_config_block_fields
class TypographyThemeConfigBlockType(ThemeConfigBlockType):
type = "typography"
model_class = TypographyThemeConfigBlock
allowed_fields = [
"heading_1_font_size",
"heading_1_text_color",
"heading_2_font_size",
"heading_2_text_color",
"heading_3_font_size",
"heading_3_text_color",
]
serializer_field_names = [
"heading_1_font_size",
"heading_1_text_color",
"heading_2_font_size",
"heading_2_text_color",
"heading_3_font_size",
"heading_3_text_color",
]
def import_serialized(self, parent, serialized_values, id_mapping):
# Translate from old color property names to new names for compat with templates
for level in range(3):
if f"heading_{level+1}_color" in serialized_values:
serialized_values[
f"heading_{level+1}_text_color"
] = serialized_values.pop(f"heading_{level+1}_color")
return super().import_serialized(parent, serialized_values, id_mapping)
class ButtonThemeConfigBlockType(ThemeConfigBlockType):
type = "button"
model_class = ButtonThemeConfigBlock
allowed_fields = ["button_background_color", "button_hover_background_color"]
serializer_field_names = [
"button_background_color",
"button_hover_background_color",
]

View file

@ -16,6 +16,7 @@ class ElementDict(TypedDict):
visibility: str
role_type: str
roles: list
styles: object
style_border_top_color: str
style_border_top_size: int
style_padding_top: int

View file

@ -1014,6 +1014,7 @@ class LocalBaserowGetRowUserServiceType(
return resolved_values
try:
dispatch_context.reset_call_stack()
resolved_values["row_id"] = ensure_integer(
resolve_formula(
service.row_id,
@ -1346,9 +1347,6 @@ class LocalBaserowUpsertRowServiceType(LocalBaserowTableServiceType):
)
raise ServiceImproperlyConfigured(message) from e
except Exception as e:
import traceback
traceback.print_exc()
message = (
"Unknown error in formula for "
f"field {field_mapping.field.name}({field_mapping.field.id}): {str(e)}"
@ -1425,7 +1423,13 @@ class LocalBaserowUpsertRowServiceType(LocalBaserowTableServiceType):
# Transform and validate the resolved value with the field type's DRF field.
serializer_field = field_type.get_serializer_field(field.specific)
resolved_value = serializer_field.run_validation(resolved_value)
try:
resolved_value = serializer_field.run_validation(resolved_value)
except DRFValidationError as exc:
raise ServiceImproperlyConfigured(
"The result value of the formula is not valid for the "
f"field `{field.name} ({field.db_column})`: {str(exc)}"
) from exc
# Then transform and validate the resolved value for prepare value for db.
try:

View file

@ -59,7 +59,6 @@ class AutoSingleRelatedObjectDescriptor(ReverseOneToOneDescriptor):
https://github.com/skorokithakis/django-annoying/blob/master/annoying/fields.py
"""
@atomic
def __get__(self, instance, instance_type=None):
model = getattr(self.related, "related_model", self.related.model)
@ -68,15 +67,18 @@ class AutoSingleRelatedObjectDescriptor(ReverseOneToOneDescriptor):
instance, instance_type
)
except model.DoesNotExist:
# Using get_or_create instead() of save() or create() as it better handles
# race conditions
obj, _ = model.objects.get_or_create(**{self.related.field.name: instance})
with atomic():
# Using get_or_create instead() of save() or create() as it better
# handles race conditions
obj, _ = model.objects.get_or_create(
**{self.related.field.name: instance}
)
# Update Django's cache, otherwise first 2 calls to obj.relobj
# will return 2 different in-memory objects
self.related.set_cached_value(instance, obj)
self.related.field.set_cached_value(obj, instance)
return obj
# Update Django's cache, otherwise first 2 calls to obj.relobj
# will return 2 different in-memory objects
self.related.set_cached_value(instance, obj)
self.related.field.set_cached_value(obj, instance)
return obj
class AutoOneToOneField(models.OneToOneField):

View file

@ -1042,3 +1042,62 @@ def get_baserow_saas_base_url() -> [str, dict]:
headers["Host"] = "localhost"
return base_url, headers
def hex_to_rgba(hex_color: str) -> tuple:
"""
Convert a hexadecimal color to an RGBA tuple.
:param hex_color: The color in hexadecimal format.
:return: The color as an (R, G, B, A) tuple.
"""
hex_color = hex_color.lstrip("#")
if len(hex_color) == 6:
hex_color += "ff" # Add full opacity if alpha is not specified
return tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4, 6))
def rgba_to_hex(rgba: tuple):
"""
Convert an RGBA tuple to a hexadecimal color.
Parameters:
:param rgba : The color as an (R, G, B, A) tuple.
:return: The color in hexadecimal format.
"""
return "#{:02x}{:02x}{:02x}{:02x}".format(*rgba)
def lighten_color(hex_color: str, factor: float):
"""
Lighten a hexadecimal color with alpha by a given factor.
:param hex_color: The original color in hexadecimal format.
:param factor: The factor to lighten the color by. Should be between 0 and 1.
A factor of 0 returns the original color, while a factor of 1 returns white.
:return: The lightened color in hexadecimal format.
"""
# Convert hex color to RGBA
rgba = hex_to_rgba(hex_color)
# Lighten the RGB part of the RGBA color
lightened_rgb = tuple(
int(channel + (255 - channel) * factor) for channel in rgba[:3]
)
# Keep the alpha channel unchanged
alpha = rgba[3]
# Combine the lightened RGB with the original alpha
lightened_rgba = lightened_rgb + (alpha,)
# Convert the lightened RGBA color back to hex
return rgba_to_hex(lightened_rgba)

View file

@ -57,11 +57,13 @@ def test_get_public_builder_by_domain_name(api_client, data_fixture):
"secondary_color": "#0eaa42ff",
"border_color": "#d7d8d9ff",
"heading_1_font_size": 24,
"heading_1_color": "#070810ff",
"heading_1_text_color": "#070810ff",
"heading_2_font_size": 20,
"heading_2_color": "#070810ff",
"heading_2_text_color": "#070810ff",
"heading_3_font_size": 16,
"heading_3_color": "#070810ff",
"heading_3_text_color": "#070810ff",
"button_background_color": "primary",
"button_hover_background_color": "#96baf6ff",
},
"user_sources": [],
}
@ -157,11 +159,13 @@ def test_get_public_builder_by_id(api_client, data_fixture):
"secondary_color": "#0eaa42ff",
"border_color": "#d7d8d9ff",
"heading_1_font_size": 24,
"heading_1_color": "#070810ff",
"heading_1_text_color": "#070810ff",
"heading_2_font_size": 20,
"heading_2_color": "#070810ff",
"heading_2_text_color": "#070810ff",
"heading_3_font_size": 16,
"heading_3_color": "#070810ff",
"heading_3_text_color": "#070810ff",
"button_background_color": "primary",
"button_hover_background_color": "#96baf6ff",
},
"user_sources": [],
}

View file

@ -7,6 +7,7 @@ from rest_framework.status import HTTP_200_OK
from baserow.api.user_files.serializers import UserFileSerializer
from baserow.contrib.builder.models import Builder
from baserow.contrib.builder.theme.registries import theme_config_block_registry
@pytest.mark.django_db
@ -64,8 +65,12 @@ def test_list_builder_applications_equal_number_of_queries_n_builders(
data_fixture.create_builder_application(workspace=workspace, order=1)
data_fixture.create_builder_application(workspace=workspace, order=2)
# Force the `MainThemeConfigBlockType` to be created.
[builder.mainthemeconfigblock for builder in Builder.objects.all()]
# Force the `ThemeConfigBlockType` to be created.
for theme_config_block_type in theme_config_block_registry.get_all():
related_name = theme_config_block_type.related_name_in_builder_model
[
getattr(builder, related_name) for builder in Builder.objects.all()
] # noqa: W0106
with CaptureQueriesContext(connection) as queries_request_1:
response = api_client.get(
@ -74,9 +79,12 @@ def test_list_builder_applications_equal_number_of_queries_n_builders(
)
assert response.status_code == HTTP_200_OK
# Force the `MainThemeConfigBlockType` to be created.
data_fixture.create_builder_application(workspace=workspace, order=2)
[builder.mainthemeconfigblock for builder in Builder.objects.all()]
# Force the `ThemeConfigBlockType` to be created.
for theme_config_block_type in theme_config_block_registry.get_all():
related_name = theme_config_block_type.related_name_in_builder_model
[
getattr(builder, related_name) for builder in Builder.objects.all()
] # noqa: W0106
with CaptureQueriesContext(connection) as queries_request_2:
response = api_client.get(
@ -87,13 +95,8 @@ def test_list_builder_applications_equal_number_of_queries_n_builders(
# The number of queries should not increase because another builder application
# is added, with its own theme.
assert (
len(queries_request_1.captured_queries)
# The -2 queries are expected because that's another a
# savepoint + release savepoint. This is unrelated to the builder application
# specific code.
== len(queries_request_2.captured_queries) - 2
assert len(queries_request_1.captured_queries) == len(
queries_request_2.captured_queries
)
@ -110,8 +113,8 @@ def test_list_builder_applications_theme(
)
data_fixture.create_builder_application(workspace=workspace, order=2)
application_1.mainthemeconfigblock.primary_color = "#ccccccff"
application_1.mainthemeconfigblock.save()
application_1.colorthemeconfigblock.primary_color = "#ccccccff"
application_1.colorthemeconfigblock.save()
url = reverse("api:applications:list", kwargs={"workspace_id": workspace.id})
@ -126,22 +129,26 @@ def test_list_builder_applications_theme(
"secondary_color": "#0eaa42ff",
"border_color": "#d7d8d9ff",
"heading_1_font_size": 24,
"heading_1_color": "#070810ff",
"heading_1_text_color": "#070810ff",
"heading_2_font_size": 20,
"heading_2_color": "#070810ff",
"heading_2_text_color": "#070810ff",
"heading_3_font_size": 16,
"heading_3_color": "#070810ff",
"heading_3_text_color": "#070810ff",
"button_background_color": "primary",
"button_hover_background_color": "#96baf6ff",
}
assert response_json[1]["theme"] == {
"primary_color": "#5190efff",
"secondary_color": "#0eaa42ff",
"border_color": "#d7d8d9ff",
"heading_1_font_size": 24,
"heading_1_color": "#070810ff",
"heading_1_text_color": "#070810ff",
"heading_2_font_size": 20,
"heading_2_color": "#070810ff",
"heading_2_text_color": "#070810ff",
"heading_3_font_size": 16,
"heading_3_color": "#070810ff",
"heading_3_text_color": "#070810ff",
"button_background_color": "primary",
"button_hover_background_color": "#96baf6ff",
}
@ -188,11 +195,13 @@ def test_get_builder_application(api_client, data_fixture):
"secondary_color": "#0eaa42ff",
"border_color": "#d7d8d9ff",
"heading_1_font_size": 24,
"heading_1_color": "#070810ff",
"heading_1_text_color": "#070810ff",
"heading_2_font_size": 20,
"heading_2_color": "#070810ff",
"heading_2_text_color": "#070810ff",
"heading_3_font_size": 16,
"heading_3_color": "#070810ff",
"heading_3_text_color": "#070810ff",
"button_background_color": "primary",
"button_hover_background_color": "#96baf6ff",
},
}
@ -241,11 +250,13 @@ def test_list_builder_applications(api_client, data_fixture):
"secondary_color": "#0eaa42ff",
"border_color": "#d7d8d9ff",
"heading_1_font_size": 24,
"heading_1_color": "#070810ff",
"heading_1_text_color": "#070810ff",
"heading_2_font_size": 20,
"heading_2_color": "#070810ff",
"heading_2_text_color": "#070810ff",
"heading_3_font_size": 16,
"heading_3_color": "#070810ff",
"heading_3_text_color": "#070810ff",
"button_background_color": "primary",
"button_hover_background_color": "#96baf6ff",
},
}
]

View file

@ -219,6 +219,7 @@ def test_builder_application_export(data_fixture):
"style_padding_left": 20,
"style_padding_right": 20,
"style_background": "none",
"styles": {},
"value": element1.value,
"level": element1.level,
"alignment": "left",
@ -247,6 +248,7 @@ def test_builder_application_export(data_fixture):
"style_padding_left": 20,
"style_padding_right": 20,
"style_background": "none",
"styles": {},
"value": element2.value,
"alignment": "left",
"roles": [],
@ -274,6 +276,7 @@ def test_builder_application_export(data_fixture):
"style_padding_left": 20,
"style_padding_right": 20,
"style_background": "none",
"styles": {},
"order": str(element_container.order),
"column_amount": 3,
"column_gap": 50,
@ -302,6 +305,7 @@ def test_builder_application_export(data_fixture):
"style_padding_left": 20,
"style_padding_right": 20,
"style_background": "none",
"styles": {},
"order": str(element_inside_container.order),
"value": element_inside_container.value,
"alignment": "left",
@ -376,6 +380,7 @@ def test_builder_application_export(data_fixture):
"style_padding_left": 20,
"style_padding_right": 20,
"style_background": "none",
"styles": {},
"value": element3.value,
"level": element3.level,
"alignment": "left",
@ -412,6 +417,7 @@ def test_builder_application_export(data_fixture):
"style_padding_left": 20,
"style_padding_right": 20,
"style_background": "none",
"styles": {},
"items_per_page": 42,
"data_source_id": element4.data_source.id,
"fields": [
@ -460,15 +466,17 @@ def test_builder_application_export(data_fixture):
},
],
"theme": {
"button_background_color": "primary",
"button_hover_background_color": "#96baf6ff",
"primary_color": "#5190efff",
"secondary_color": "#0eaa42ff",
"border_color": "#d7d8d9ff",
"heading_1_font_size": 24,
"heading_1_color": "#070810ff",
"heading_1_text_color": "#070810ff",
"heading_2_font_size": 20,
"heading_2_color": "#070810ff",
"heading_2_text_color": "#070810ff",
"heading_3_font_size": 16,
"heading_3_color": "#070810ff",
"heading_3_text_color": "#070810ff",
},
"id": builder.id,
"name": builder.name,
@ -744,11 +752,13 @@ IMPORT_REFERENCE = {
"secondary_color": "#ccccccff",
"border_color": "#ccccccff",
"heading_1_font_size": 25,
"heading_1_color": "#ccccccff",
"heading_1_text_color": "#ccccccff",
"heading_2_font_size": 21,
"heading_2_color": "#ccccccff",
"heading_2_color": "#ccccccff", # Old property name
"heading_3_font_size": 17,
"heading_3_color": "#ccccccff",
"button_background_color": "#ccccccff",
"button_hover_background_color": "#ccccccff",
},
"id": 999,
"name": "Holly Sherman",
@ -789,15 +799,22 @@ def test_builder_application_import(data_fixture):
assert first_data_source.name == "source 2"
assert first_data_source.service.integration.id == first_integration.id
theme_config_block = builder.mainthemeconfigblock
assert theme_config_block.heading_1_color == "#ccccccff"
assert theme_config_block.heading_1_font_size == 25
assert theme_config_block.heading_2_color == "#ccccccff"
assert theme_config_block.heading_2_font_size == 21
assert theme_config_block.heading_3_color == "#ccccccff"
assert theme_config_block.heading_3_font_size == 17
assert theme_config_block.primary_color == "#ccccccff"
assert theme_config_block.secondary_color == "#ccccccff"
typography_config_block = builder.typographythemeconfigblock
assert typography_config_block.heading_1_text_color == "#ccccccff"
assert typography_config_block.heading_1_font_size == 25
assert typography_config_block.heading_2_text_color == "#ccccccff"
assert typography_config_block.heading_2_font_size == 21
assert typography_config_block.heading_3_text_color == "#ccccccff"
assert typography_config_block.heading_3_font_size == 17
color_config_block = builder.colorthemeconfigblock
assert color_config_block.primary_color == "#ccccccff"
assert color_config_block.secondary_color == "#ccccccff"
assert color_config_block.border_color == "#ccccccff"
button_config_block = builder.buttonthemeconfigblock
assert button_config_block.button_background_color == "#ccccccff"
assert button_config_block.button_hover_background_color == "#ccccccff"
[
element1,
@ -918,9 +935,9 @@ IMPORT_REFERENCE_COMPLEX = {
"secondary_color": "#ccccccff",
"border_color": "#ccccccff",
"heading_1_font_size": 25,
"heading_1_color": "#ccccccff",
"heading_1_text_color": "#ccccccff",
"heading_2_font_size": 21,
"heading_2_color": "#ccccccff",
"heading_2_color": "#ccccccff", # Old property name
"heading_3_font_size": 17,
"heading_3_color": "#ccccccff",
},

View file

@ -21,11 +21,13 @@ def test_get_builder_select_related_theme_config(
data_fixture, django_assert_num_queries
):
builder = data_fixture.create_builder_application()
builder.mainthemeconfigblock
builder.colorthemeconfigblock
builder.typographythemeconfigblock
builder.buttonthemeconfigblock
builder = BuilderHandler().get_builder(builder.id)
# We expect 0 queries here but as Django logs transaction
# operations we have 2 of them here
with django_assert_num_queries(2):
builder.mainthemeconfigblock.id
with django_assert_num_queries(0):
builder.colorthemeconfigblock.id
builder.typographythemeconfigblock.id
builder.buttonthemeconfigblock.id

View file

@ -51,6 +51,7 @@ def test_0010_remove_orphan_collection_fields_forwards(
@pytest.mark.once_per_day_in_ci
# You must add --run-once-per-day-in-ci to execute this test
def test_0018_resolve_collection_field_configs(migrator, teardown_table_metadata):
migrate_from = [
("builder", "0017_repeatelement"),
@ -119,3 +120,164 @@ def test_0018_resolve_collection_field_configs(migrator, teardown_table_metadata
field_without_page_parameters.refresh_from_db()
assert field_without_page_parameters.config["page_parameters"] == []
@pytest.mark.once_per_day_in_ci
# You must add --run-once-per-day-in-ci to execute this test
def test_0025_theme_config_block(migrator, teardown_table_metadata):
migrate_from = [
("builder", "0024_element_role_type_element_roles"),
]
migrate_to = [
("builder", "0025_buttonthemeconfigblock_colorthemeconfigblock_and_more"),
]
old_state = migrator.migrate(migrate_from)
ContentType = old_state.apps.get_model("contenttypes", "ContentType")
Workspace = old_state.apps.get_model("core", "Workspace")
Builder = old_state.apps.get_model("builder", "Builder")
workspace = Workspace.objects.create(name="Workspace")
builder = Builder.objects.create(
order=2,
name="Builder",
workspace=workspace,
content_type=ContentType.objects.get_for_model(Builder),
)
builder.mainthemeconfigblock.primary_color = "#f00000ff"
builder.mainthemeconfigblock.secondary_color = "#0eaa42ff"
builder.mainthemeconfigblock.heading_1_font_size = "#0eaa42ff"
builder.mainthemeconfigblock.heading_1_font_size = 30
builder.mainthemeconfigblock.heading_1_color = "#ff0000ff"
builder.mainthemeconfigblock.heading_2_font_size = 20
builder.mainthemeconfigblock.heading_2_color = "#070810ff"
builder.mainthemeconfigblock.heading_3_font_size = 16
builder.mainthemeconfigblock.heading_3_color = "#070810ff"
builder.mainthemeconfigblock.save()
new_state = migrator.migrate(migrate_to)
Builder = new_state.apps.get_model("builder", "Builder")
builder = Builder.objects.first()
assert builder.colorthemeconfigblock.primary_color == "#f00000ff"
assert builder.colorthemeconfigblock.secondary_color == "#0eaa42ff"
assert builder.typographythemeconfigblock.heading_1_font_size == 30
assert builder.typographythemeconfigblock.heading_1_text_color == "#ff0000ff"
assert builder.typographythemeconfigblock.heading_2_font_size == 20
assert builder.typographythemeconfigblock.heading_2_text_color == "#070810ff"
assert builder.typographythemeconfigblock.heading_3_font_size == 16
assert builder.typographythemeconfigblock.heading_3_text_color == "#070810ff"
assert builder.buttonthemeconfigblock.button_background_color == "primary"
@pytest.mark.once_per_day_in_ci
# You must add --run-once-per-day-in-ci to execute this test
def test_0025_element_properties(migrator, teardown_table_metadata):
migrate_from = [
("builder", "0024_element_role_type_element_roles"),
]
migrate_to = [
("builder", "0025_buttonthemeconfigblock_colorthemeconfigblock_and_more"),
]
old_state = migrator.migrate(migrate_from)
ContentType = old_state.apps.get_model("contenttypes", "ContentType")
Workspace = old_state.apps.get_model("core", "Workspace")
Builder = old_state.apps.get_model("builder", "Builder")
Page = old_state.apps.get_model("builder", "Page")
TableElement = old_state.apps.get_model("builder", "TableElement")
HeadingElement = old_state.apps.get_model("builder", "HeadingElement")
ButtonElement = old_state.apps.get_model("builder", "ButtonElement")
LinkElement = old_state.apps.get_model("builder", "LinkElement")
FormContainerElement = old_state.apps.get_model("builder", "FormContainerElement")
workspace = Workspace.objects.create(name="Workspace")
builder = Builder.objects.create(
order=2,
name="Builder",
workspace=workspace,
content_type=ContentType.objects.get_for_model(Builder),
)
page = Page.objects.create(order=2, builder=builder, name="Page", path="page/")
TableElement.objects.create(
order=1,
page=page,
items_per_page=5,
content_type=ContentType.objects.get_for_model(TableElement),
button_color="#CCCCCCCC",
)
ButtonElement.objects.create(
order=2,
page=page,
button_color="#CC00CCCC",
content_type=ContentType.objects.get_for_model(ButtonElement),
)
LinkElement.objects.create(
order=2,
page=page,
button_color="#CCCC00CC",
content_type=ContentType.objects.get_for_model(LinkElement),
)
FormContainerElement.objects.create(
order=2,
page=page,
button_color="#CCCCCC00",
content_type=ContentType.objects.get_for_model(FormContainerElement),
)
HeadingElement.objects.create(
order=2,
page=page,
font_color="#0000CCCC",
content_type=ContentType.objects.get_for_model(HeadingElement),
)
new_state = migrator.migrate(migrate_to)
ButtonElement = new_state.apps.get_model("builder", "ButtonElement")
HeadingElement = new_state.apps.get_model("builder", "HeadingElement")
LinkElement = new_state.apps.get_model("builder", "LinkElement")
FormContainerElement = new_state.apps.get_model("builder", "FormContainerElement")
TableElement = new_state.apps.get_model("builder", "TableElement")
heading = HeadingElement.objects.first()
button = ButtonElement.objects.first()
link = LinkElement.objects.first()
form = FormContainerElement.objects.first()
table = TableElement.objects.first()
assert table.styles == {
"button": {
"button_background_color": "#CCCCCCCC",
"button_hover_background_color": "#dbdbdbcc",
}
}
assert button.styles == {
"button": {
"button_background_color": "#CC00CCCC",
"button_hover_background_color": "#db4cdbcc",
}
}
assert link.styles == {
"button": {
"button_background_color": "#CCCC00CC",
"button_hover_background_color": "#dbdb4ccc",
}
}
assert form.styles == {
"button": {
"button_background_color": "#CCCCCC00",
"button_hover_background_color": "#dbdbdb00",
}
}
assert heading.styles == {
"typography": {
"heading_1_text_color": "#0000CCCC",
}
}

View file

@ -1,6 +1,7 @@
import pytest
from baserow.contrib.builder.theme.handler import ThemeHandler
from baserow.contrib.builder.theme.registries import theme_config_block_registry
@pytest.mark.django_db
@ -11,16 +12,20 @@ def test_update_theme(data_fixture):
builder,
primary_color="#f00000ff",
heading_1_font_size=30,
heading_1_color="#ff0000ff",
heading_1_text_color="#ff0000ff",
button_background_color="#ccddcc",
)
builder.mainthemeconfigblock.refresh_from_db()
for theme_config_block_type in theme_config_block_registry.get_all():
related_name = theme_config_block_type.related_name_in_builder_model
getattr(builder, related_name).refresh_from_db()
assert builder.mainthemeconfigblock.primary_color == "#f00000ff"
assert builder.mainthemeconfigblock.secondary_color == "#0eaa42ff"
assert builder.mainthemeconfigblock.heading_1_font_size == 30
assert builder.mainthemeconfigblock.heading_1_color == "#ff0000ff"
assert builder.mainthemeconfigblock.heading_2_font_size == 20
assert builder.mainthemeconfigblock.heading_2_color == "#070810ff"
assert builder.mainthemeconfigblock.heading_3_font_size == 16
assert builder.mainthemeconfigblock.heading_3_color == "#070810ff"
assert builder.colorthemeconfigblock.primary_color == "#f00000ff"
assert builder.colorthemeconfigblock.secondary_color == "#0eaa42ff"
assert builder.typographythemeconfigblock.heading_1_font_size == 30
assert builder.typographythemeconfigblock.heading_1_text_color == "#ff0000ff"
assert builder.typographythemeconfigblock.heading_2_font_size == 20
assert builder.typographythemeconfigblock.heading_2_text_color == "#070810ff"
assert builder.typographythemeconfigblock.heading_3_font_size == 16
assert builder.typographythemeconfigblock.heading_3_text_color == "#070810ff"
assert builder.buttonthemeconfigblock.button_background_color == "#ccddcc"

View file

@ -37,5 +37,9 @@ def test_update_theme_updated(data_fixture):
user = data_fixture.create_user()
builder = data_fixture.create_builder_application(user=user)
builder = ThemeService().update_theme(user, builder, primary_color="#f00000ff")
assert builder.mainthemeconfigblock.primary_color == "#f00000ff"
builder = ThemeService().update_theme(
user, builder, primary_color="#f00000ff", heading_1_font_size=42
)
assert builder.colorthemeconfigblock.primary_color == "#f00000ff"
assert builder.typographythemeconfigblock.heading_1_font_size == 42

View file

@ -7,7 +7,6 @@ import pytest
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework.exceptions import ValidationError
from rest_framework.exceptions import ValidationError as DRFValidationError
from rest_framework.fields import (
BooleanField,
CharField,
@ -2248,7 +2247,7 @@ def test_local_baserow_upsert_row_service_dispatch_data_incompatible_value(
dispatch_context = BuilderDispatchContext(Mock(), page)
field_mapping = service.field_mappings.create(field=boolean_field, value="'Horse'")
with pytest.raises(DRFValidationError) as exc:
with pytest.raises(ServiceImproperlyConfigured) as exc:
service_type.dispatch_data(
service, {"table": table, field_mapping.id: "Horse"}, dispatch_context
)

View file

@ -362,7 +362,6 @@ def test_import_user_source(data_fixture):
builder, TO_IMPORT, defaultdict(MirrorDict)
)
assert imported_instance.id != 28
assert imported_instance.integration_id == integration.id
assert imported_instance.name == "Test name"

View file

@ -0,0 +1,7 @@
{
"type": "feature",
"message": "[Builder] Improved application styling capabilities",
"issue_number": 2388,
"bullet_points": [],
"created_at": "2024-06-07"
}

View file

@ -1,10 +1,11 @@
<template>
<div
class="button-element"
:class="classes"
:style="{
'--button-color': resolveColor(element.button_color, colorVariables),
:class="{
[`element--alignment-horizontal-${element.alignment}`]: true,
'element--no-value': !resolvedValue,
}"
:style="getStyleOverride('button')"
>
<ABButton
:full-width="element.width === WIDTHS.FULL.value"
@ -43,17 +44,7 @@ export default {
computed: {
WIDTHS: () => WIDTHS,
resolvedValue() {
try {
return ensureString(this.resolveFormula(this.element.value))
} catch {
return ''
}
},
classes() {
return {
[`element--alignment-horizontal-${this.element.alignment}`]: true,
'element--no-value': !this.resolvedValue,
}
return ensureString(this.resolveFormula(this.element.value))
},
},
}

View file

@ -1,9 +1,5 @@
<template>
<div
:style="{
'--button-color': resolveColor(element.button_color, colorVariables),
}"
>
<div :style="getStyleOverride('button')">
<div
v-if="
mode === 'editing' &&

View file

@ -5,15 +5,7 @@
[`element--alignment-horizontal-${element.alignment}`]: true,
}"
>
<ABHeading
:level="element.level"
:style="{
[`--heading-h${element.level}-color`]: resolveColor(
element.font_color,
headingColorVariables
),
}"
>
<ABHeading :level="element.level" :style="getStyleOverride('typography')">
{{ resolvedValue || $t('headingElement.noValue') }}
</ABHeading>
</div>
@ -21,12 +13,11 @@
<script>
import element from '@baserow/modules/builder/mixins/element'
import headingElement from '@baserow/modules/builder/mixins/headingElement'
import { ensureString } from '@baserow/modules/core/utils/validator'
export default {
name: 'HeadingElement',
mixins: [element, headingElement],
mixins: [element],
props: {
/**
* @type {Object}

View file

@ -2,9 +2,7 @@
<div
class="link-element"
:class="classes"
:style="{
'--button-color': resolveColor(element.button_color, colorVariables),
}"
:style="getStyleOverride('button')"
>
<ABLink
:full-width="element.width === WIDTHS.FULL.value"

View file

@ -1,10 +1,5 @@
<template>
<div
:style="{
'--button-color': resolveColor(element.button_color, colorVariables),
}"
class="table-element"
>
<div :style="getStyleOverride('button')" class="table-element">
<BaserowTable
:fields="element.fields"
:rows="rows"

View file

@ -30,13 +30,16 @@
/>
</template>
</ApplicationBuilderFormulaInputGroup>
<ColorInputGroup
<FormGroup
v-else
v-model="values.colors"
:label="$t('tagsFieldForm.fieldColorsLabel')"
:color-variables="colorVariables"
horizontal
:label="$t('tagsFieldForm.fieldColorsLabel')"
>
<ColorInput
v-model="values.colors"
:color-variables="colorVariables"
small
/>
<template #after-input>
<ButtonIcon
icon="iconoir-sigma-function"
@ -44,7 +47,7 @@
@click="setColorsToFormula"
/>
</template>
</ColorInputGroup>
</FormGroup>
</div>
</form>
</template>
@ -69,7 +72,7 @@ export default {
allowedValues: ['values', 'colors', 'colors_is_formula'],
values: {
values: '',
colors: '',
colors: '#acc8f8',
colors_is_formula: false,
},
}
@ -82,11 +85,11 @@ export default {
methods: {
setColorsToFormula() {
this.values.colors_is_formula = true
this.values.colors = ''
this.values.colors = `'${this.values.colors}'`
},
setColorsToPicker() {
this.values.colors_is_formula = false
this.values.colors = ''
this.values.colors = '#acc8f8'
},
},
}

View file

@ -1,31 +1,29 @@
<template>
<form @submit.prevent @keydown.enter.prevent>
<CustomStyle
v-model="values.styles"
style-key="button"
:config-block-types="['button']"
:theme="builder.theme"
:element="values"
/>
<ApplicationBuilderFormulaInputGroup
v-model="values.value"
:label="$t('buttonElementForm.valueLabel')"
:placeholder="$t('buttonElementForm.valuePlaceholder')"
:data-providers-allowed="DATA_PROVIDERS_ALLOWED_ELEMENTS"
/>
<FormElement class="control">
<HorizontalAlignmentsSelector v-model="values.alignment" />
</FormElement>
<FormElement class="control">
<WidthSelector v-model="values.width" />
</FormElement>
<ColorInputGroup
v-model="values.button_color"
:label="$t('buttonElementForm.buttonColor')"
:color-variables="colorVariables"
/>
<HorizontalAlignmentsSelector v-model="values.alignment" />
<WidthSelector v-model="values.width" />
</form>
</template>
<script>
import ApplicationBuilderFormulaInputGroup from '@baserow/modules/builder/components/ApplicationBuilderFormulaInputGroup'
import { HORIZONTAL_ALIGNMENTS, WIDTHS } from '@baserow/modules/builder/enums'
import CustomStyle from '@baserow/modules/builder/components/elements/components/forms/style/CustomStyle'
import HorizontalAlignmentsSelector from '@baserow/modules/builder/components/elements/components/forms/general/settings/HorizontalAlignmentsSelector'
import WidthSelector from '@baserow/modules/builder/components/elements/components/forms/general/settings/WidthSelector'
import { themeToColorVariables } from '@baserow/modules/builder/utils/theme'
import elementForm from '@baserow/modules/builder/mixins/elementForm'
export default {
@ -34,6 +32,7 @@ export default {
WidthSelector,
ApplicationBuilderFormulaInputGroup,
HorizontalAlignmentsSelector,
CustomStyle,
},
mixins: [elementForm],
data() {
@ -42,13 +41,10 @@ export default {
value: '',
alignment: HORIZONTAL_ALIGNMENTS.LEFT.value,
width: WIDTHS.AUTO.value,
styles: {},
},
allowedValues: ['value', 'alignment', 'width', 'styles'],
}
},
computed: {
colorVariables() {
return themeToColorVariables(this.builder.theme)
},
},
}
</script>

View file

@ -1,16 +1,18 @@
<template>
<form @submit.prevent @keydown.enter.prevent>
<CustomStyle
v-model="values.styles"
style-key="button"
:config-block-types="['button']"
:theme="builder.theme"
:element="values"
/>
<ApplicationBuilderFormulaInputGroup
v-model="values.submit_button_label"
:data-providers-allowed="DATA_PROVIDERS_ALLOWED_ELEMENTS"
:label="$t('formContainerElementForm.submitButtonLabel')"
:placeholder="$t('formContainerElementForm.submitButtonPlaceholder')"
/>
<ColorInputGroup
v-model="values.button_color"
:label="$t('formContainerElementForm.buttonColor')"
:color-variables="colorVariables"
/>
<FormGroup
:label="$t('formContainerElementForm.resetToInitialValuesTitle')"
:description="
@ -27,17 +29,24 @@
<script>
import ApplicationBuilderFormulaInputGroup from '@baserow/modules/builder/components/ApplicationBuilderFormulaInputGroup.vue'
import elementForm from '@baserow/modules/builder/mixins/elementForm'
import CustomStyle from '@baserow/modules/builder/components/elements/components/forms/style/CustomStyle'
export default {
name: 'FormContainerElementForm',
components: { ApplicationBuilderFormulaInputGroup },
components: { ApplicationBuilderFormulaInputGroup, CustomStyle },
mixins: [elementForm],
data() {
return {
values: {
submit_button_label: '',
reset_initial_values_post_submission: false,
styles: {},
},
allowedValues: [
'submit_button_label',
'reset_initial_values_post_submission',
'styles',
],
}
},
}

View file

@ -1,5 +1,13 @@
<template>
<form @submit.prevent @keydown.enter.prevent>
<CustomStyle
v-if="values.level < 4"
v-model="values.styles"
style-key="typography"
:config-block-types="['typography']"
:theme="builder.theme"
:element="values"
/>
<FormGroup :label="$t('headingElementForm.levelTitle')">
<Dropdown v-model="values.level" :show-search="false">
<DropdownItem
@ -21,42 +29,37 @@
<FormElement class="control">
<HorizontalAlignmentsSelector v-model="values.alignment" />
</FormElement>
<FontSelector
:default-values="defaultValues"
:color-variables="headingColorVariables"
@values-changed="$emit('values-changed', $event)"
></FontSelector>
</form>
</template>
<script>
import ApplicationBuilderFormulaInputGroup from '@baserow/modules/builder/components/ApplicationBuilderFormulaInputGroup'
import headingElement from '@baserow/modules/builder/mixins/headingElement'
import FontSelector from '@baserow/modules/builder/components/elements/components/forms/general/settings/FontSelector'
import elementForm from '@baserow/modules/builder/mixins/elementForm'
import HorizontalAlignmentsSelector from '@baserow/modules/builder/components/elements/components/forms/general/settings/HorizontalAlignmentsSelector.vue'
import { HORIZONTAL_ALIGNMENTS } from '@baserow/modules/builder/enums'
import CustomStyle from '../style/CustomStyle.vue'
export default {
name: 'HeaderElementForm',
components: {
HorizontalAlignmentsSelector,
FontSelector,
ApplicationBuilderFormulaInputGroup,
CustomStyle,
},
mixins: [elementForm, headingElement],
mixins: [elementForm],
data() {
return {
values: {
value: '',
level: 1,
alignment: HORIZONTAL_ALIGNMENTS.LEFT.value,
styles: {},
},
levels: [...Array(6).keys()].map((level) => ({
name: this.$t('headingElementForm.headingName', { level: level + 1 }),
value: level + 1,
})),
allowedValues: ['value', 'level', 'alignment'],
allowedValues: ['value', 'level', 'alignment', 'styles'],
}
},
}

View file

@ -1,39 +1,40 @@
<template>
<form @submit.prevent @keydown.enter.prevent>
<CustomStyle
v-if="values.variant === 'button'"
v-model="values.styles"
style-key="button"
:config-block-types="['button']"
:theme="builder.theme"
:element="values"
/>
<ApplicationBuilderFormulaInputGroup
v-model="values.value"
:label="$t('linkElementForm.text')"
:placeholder="$t('linkElementForm.textPlaceholder')"
:data-providers-allowed="DATA_PROVIDERS_ALLOWED_ELEMENTS"
/>
<LinkNavigationSelectionForm
:default-values="defaultValues"
@values-changed="emitChange($event)"
/>
<FormElement class="control">
<label class="control__label">
{{ $t('linkElementForm.variant') }}
</label>
<div class="control__elements control__elements--flex">
<FormGroup :label="$t('linkElementForm.variant')">
<div class="control__elements--flex">
<RadioButton v-model="values.variant" value="link">
{{ $t('linkElementForm.variantLink') }} </RadioButton
><RadioButton v-model="values.variant" value="button">
{{ $t('linkElementForm.variantLink') }}
</RadioButton>
<RadioButton v-model="values.variant" value="button">
{{ $t('linkElementForm.variantButton') }}
</RadioButton>
</div>
</FormElement>
<FormElement class="control">
<HorizontalAlignmentSelector v-model="values.alignment" />
</FormElement>
<FormElement v-if="values.variant === 'button'" class="control">
</FormGroup>
<HorizontalAlignmentSelector v-model="values.alignment" />
<template v-if="values.variant === 'button'">
<WidthSelector v-model="values.width" />
</FormElement>
<ColorInputGroup
v-if="values.variant === 'button'"
v-model="values.button_color"
:label="$t('linkElementForm.buttonColor')"
:color-variables="colorVariables"
/>
</template>
</form>
</template>
@ -45,6 +46,8 @@ import ApplicationBuilderFormulaInputGroup from '@baserow/modules/builder/compon
import elementForm from '@baserow/modules/builder/mixins/elementForm'
import LinkNavigationSelectionForm from '@baserow/modules/builder/components/elements/components/forms/general/LinkNavigationSelectionForm'
import CustomStyle from '@baserow/modules/builder/components/elements/components/forms/style/CustomStyle'
export default {
name: 'LinkElementForm',
components: {
@ -52,6 +55,7 @@ export default {
ApplicationBuilderFormulaInputGroup,
HorizontalAlignmentSelector,
LinkNavigationSelectionForm,
CustomStyle,
},
mixins: [elementForm],
data() {
@ -61,8 +65,9 @@ export default {
alignment: HORIZONTAL_ALIGNMENTS.LEFT.value,
variant: 'link',
width: WIDTHS.AUTO.value,
styles: {},
},
allowedValues: ['value', 'alignment', 'variant', 'width'],
allowedValues: ['value', 'alignment', 'variant', 'width', 'styles'],
}
},
}

View file

@ -1,5 +1,12 @@
<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 :label="$t('tableElementForm.dataSource')">
<div class="control__elements">
<div @click="userHasChangedDataSource = true">
@ -169,11 +176,6 @@
</template>
</DeviceSelector>
</FormGroup>
<ColorInputGroup
v-model="values.button_color"
:label="$t('tableElementForm.buttonColor')"
:color-variables="colorVariables"
/>
</form>
</template>
@ -195,10 +197,15 @@ import collectionElementForm from '@baserow/modules/builder/mixins/collectionEle
import { TABLE_ORIENTATION } from '@baserow/modules/builder/enums'
import DeviceSelector from '@baserow/modules/builder/components/page/header/DeviceSelector.vue'
import { mapActions, mapGetters } from 'vuex'
import CustomStyle from '@baserow/modules/builder/components/elements/components/forms/style/CustomStyle'
export default {
name: 'TableElementForm',
components: { DeviceSelector, ApplicationBuilderFormulaInputGroup },
components: {
ApplicationBuilderFormulaInputGroup,
DeviceSelector,
CustomStyle,
},
mixins: [elementForm, collectionElementForm],
data() {
return {
@ -206,14 +213,14 @@ export default {
'data_source_id',
'fields',
'items_per_page',
'button_color',
'orientation',
'styles',
],
values: {
fields: [],
data_source_id: null,
items_per_page: 1,
button_color: '',
styles: {},
orientation: {},
},
userHasChangedDataSource: false,

View file

@ -6,9 +6,7 @@
:placeholder="$t('textElementForm.textPlaceholder')"
:data-providers-allowed="DATA_PROVIDERS_ALLOWED_ELEMENTS"
/>
<FormElement class="control">
<HorizontalAlignmentsSelector v-model="values.alignment" />
</FormElement>
<HorizontalAlignmentsSelector v-model="values.alignment" />
<FormGroup :label="$t('textElementForm.textFormatTypeLabel')">
<RadioButton v-model="values.format" :value="TEXT_FORMAT_TYPES.PLAIN">
{{ $t('textElementForm.textFormatTypePlain') }}

View file

@ -1,36 +0,0 @@
<template>
<ColorInputGroup
v-model="values.font_color"
:label="$t('fontSidePanelForm.label')"
:color-variables="colorVariables"
/>
</template>
<script>
import ColorInputGroup from '@baserow/modules/core/components/ColorInputGroup'
import form from '@baserow/modules/core/mixins/form'
import { resolveColor } from '@baserow/modules/core/utils/colors'
export default {
name: 'FontSelector',
components: { ColorInputGroup },
mixins: [form],
props: {
colorVariables: {
type: Array,
required: true,
},
},
data() {
return { values: { font_color: 'primary' }, allowedValues: ['font_color'] }
},
watch: {
'values.font_color': {
handler(value) {
this.$emit('input', value)
},
},
},
methods: { resolveColor },
}
</script>

View file

@ -1,18 +1,13 @@
<template>
<div>
<label class="control__label">
{{ $t('horizontalAlignmentSelector.alignment') }}
</label>
<div class="control__elements">
<RadioGroup
v-model="selected"
:options="alignmentValues"
type="button"
@input="$emit('input', $event)"
>
</RadioGroup>
</div>
</div>
<FormGroup :label="$t('horizontalAlignmentSelector.alignment')">
<RadioGroup
v-model="selected"
:options="alignmentValues"
type="button"
@input="$emit('input', $event)"
>
</RadioGroup>
</FormGroup>
</template>
<script>

View file

@ -0,0 +1,81 @@
<template>
<div class="custom-style">
<ButtonText
class="custom-style__button"
icon="baserow-icon-settings"
@click="openPanel()"
/>
<Context ref="context">
<div class="custom-style__config-blocks">
<div
v-for="(themeConfigBlock, index) in themeConfigBlocks"
:key="themeConfigBlock.getType()"
class="custom-style__config-block"
>
<h2
v-if="themeConfigBlocks.length > 1"
class="custom-style__config-block-title"
>
{{ themeConfigBlock.label }}
</h2>
<ThemeConfigBlock
:theme="theme"
:element="element"
:default-values="value[styleKey] || {}"
:preview="false"
:theme-config-block-type="themeConfigBlock"
:class="{ 'margin-top-3': index >= 1 }"
@values-changed="onValuesChanged($event)"
/>
</div>
</div>
</Context>
</div>
</template>
<script>
import ThemeConfigBlock from '@baserow/modules/builder/components/theme/ThemeConfigBlock'
export default {
name: 'CustomStyle',
components: { ThemeConfigBlock },
props: {
value: {
type: Object,
required: false,
default: () => {},
},
theme: { type: Object, required: true },
configBlockTypes: {
type: Array,
required: true,
},
styleKey: { type: String, required: true },
element: {
type: Object,
required: true,
},
},
data() {
return {}
},
computed: {
themeConfigBlocks() {
return this.configBlockTypes.map((confType) =>
this.$registry.get('themeConfigBlock', confType)
)
},
},
methods: {
openPanel() {
this.$refs.context.toggle(this.$el, 'bottom', 'left', -100, -380)
},
onValuesChanged(newValues) {
this.$emit('input', {
...this.value,
[this.styleKey]: newValues,
})
},
},
}
</script>

View file

@ -1,17 +1,37 @@
<template>
<div class="theme-settings">
<component
:is="themeConfigBlock.component"
v-for="themeConfigBlock in themeConfigBlocks"
:key="themeConfigBlock.type"
:builder="builder"
/>
</div>
<ThemeProvider class="theme-settings">
<Tabs>
<Tab
v-for="themeConfigBlock in themeConfigBlocks"
:key="themeConfigBlock.getType()"
:title="themeConfigBlock.label"
>
<div class="padding-top-2">
<ThemeConfigBlock
:default-values="builder.theme"
:theme-config-block-type="themeConfigBlock"
@values-changed="update($event)"
/>
</div>
</Tab>
</Tabs>
</ThemeProvider>
</template>
<script>
import ThemeProvider from '@baserow/modules/builder/components/theme/ThemeProvider'
import ThemeConfigBlock from '@baserow/modules/builder/components/theme/ThemeConfigBlock'
import { mapActions } from 'vuex'
import { notifyIf } from '@baserow/modules/core/utils/error'
import _ from 'lodash'
export default {
name: 'ThemeSettings',
components: { ThemeProvider, ThemeConfigBlock },
provide() {
return { builder: this.builder }
},
props: {
builder: {
type: Object,
@ -20,7 +40,29 @@ export default {
},
computed: {
themeConfigBlocks() {
return Object.values(this.$registry.getAll('themeConfigBlock'))
return this.$registry.getOrderedList('themeConfigBlock')
},
},
methods: {
...mapActions({
setThemeProperty: 'theme/setProperty',
forceSetThemeProperty: 'theme/forceSetProperty',
}),
async update(newValues) {
const differences = Object.fromEntries(
Object.entries(newValues).filter(
([key, value]) => !_.isEqual(value, this.builder.theme[key])
)
)
try {
await Promise.all(
Object.entries(differences).map(([key, value]) =>
this.setThemeProperty({ builder: this.builder, key, value })
)
)
} catch (error) {
notifyIf(error, 'application')
}
},
},
}

View file

@ -0,0 +1,77 @@
<template>
<div>
<ThemeConfigBlockSection :title="$t('buttonThemeConfigBlock.defaultState')">
<template #default>
<FormGroup
horizontal
:label="$t('buttonThemeConfigBlock.backgroundColor')"
>
<ColorInput
v-model="values.button_background_color"
:color-variables="colorVariables"
:default-value="theme?.button_background_color"
small
/>
<template #after-input>
<ResetButton
v-model="values"
:theme="theme"
property="button_background_color"
/>
</template>
</FormGroup>
</template>
<template #preview>
<ABButton>{{ $t('buttonThemeConfigBlock.button') }}</ABButton>
</template>
</ThemeConfigBlockSection>
<ThemeConfigBlockSection :title="$t('buttonThemeConfigBlock.hoverState')">
<template #default>
<FormGroup
horizontal
:label="$t('buttonThemeConfigBlock.backgroundColor')"
>
<ColorInput
v-model="values.button_hover_background_color"
:color-variables="colorVariables"
:default-value="theme?.button_hover_background_color"
small
/>
<template #after-input>
<ResetButton
v-model="values"
:theme="theme"
property="button_hover_background_color"
/>
</template>
</FormGroup>
</template>
<template #preview>
<ABButton class="ab-button--force-hover">
{{ $t('buttonThemeConfigBlock.button') }}
</ABButton>
</template>
</ThemeConfigBlockSection>
</div>
</template>
<script>
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'
export default {
name: 'ButtonThemeConfigBlock',
components: { ThemeConfigBlockSection, ResetButton },
mixins: [themeConfigBlock],
data() {
return {
values: {},
allowedValues: [
'button_background_color',
'button_hover_background_color',
],
}
},
}
</script>

View file

@ -0,0 +1,33 @@
<template>
<ThemeConfigBlockSection>
<template #default>
<FormGroup horizontal :label="$t('colorThemeConfigBlock.primaryColor')">
<ColorInput v-model="values.primary_color" small />
</FormGroup>
<FormGroup horizontal :label="$t('colorThemeConfigBlock.secondaryColor')">
<ColorInput v-model="values.secondary_color" small />
</FormGroup>
<FormGroup horizontal :label="$t('colorThemeConfigBlock.borderColor')">
<ColorInput v-model="values.border_color" small />
</FormGroup>
</template>
</ThemeConfigBlockSection>
</template>
<script>
import themeConfigBlock from '@baserow/modules/builder/mixins/themeConfigBlock'
import ThemeConfigBlockSection from '@baserow/modules/builder/components/theme/ThemeConfigBlockSection'
export default {
name: 'ColorThemeConfigBlock',
components: { ThemeConfigBlockSection },
mixins: [themeConfigBlock],
data() {
return {
values: {},
allowedValues: ['primary_color', 'secondary_color', 'border_color'],
}
},
}
</script>

View file

@ -1,214 +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')"
@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">
<div class="control">
<div class="control__label">
{{ $t('mainThemeConfigBlock.headingLabel', { i }) }}
</div>
<div class="control__elements control__elements--flex">
<ColorInput
:value="builder.theme[`heading_${i}_color`]"
@input="setPropertyInStore(`heading_${i}_color`, $event)"
/>
<div class="input__with-icon margin-left-2">
<input
type="number"
class="input remove-number-input-controls"
:min="fontSizeMin"
:max="fontSizeMax"
:class="{
'input--error':
$v.builder.theme[`heading_${i}_font_size`].$error,
}"
:value="builder.theme[`heading_${i}_font_size`]"
@input="
;[
$v.builder.theme[`heading_${i}_font_size`].$touch(),
setPropertyInStore(
`heading_${i}_font_size`,
$event.target.value,
!$v.builder.theme[`heading_${i}_font_size`].$error
),
]
"
/>
<i>px</i>
</div>
</div>
<div
v-if="$v.builder.theme[`heading_${i}_font_size`].$error"
class="error"
>
{{ $t('error.minMaxLength', { min: 1, max: 100 }) }}
</div>
</div>
</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,38 @@
<template>
<ButtonIcon
v-if="propertyModified()"
v-tooltip="$t('resetButton.reset')"
icon="iconoir-erase"
@click="resetProperty()"
/>
</template>
<script>
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 },
},
data() {
return {}
},
methods: {
propertyModified() {
if (!this.theme) {
return false
}
return !_.isEqual(this.value[this.property], this.theme[this.property])
},
resetProperty() {
this.$emit('input', {
...this.value,
[this.property]: this.theme[this.property],
})
},
},
}
</script>

View file

@ -0,0 +1,45 @@
<template>
<div
class="theme-config-block"
:class="{
'theme-config-block--no-preview': !preview,
}"
>
<component
:is="themeConfigBlockType.component"
:preview="preview"
:theme="theme"
:element="element"
:default-values="defaultValues"
@values-changed="$emit('values-changed', $event)"
/>
</div>
</template>
<script>
export default {
name: 'ThemeConfigBlock',
props: {
defaultValues: {
type: Object,
required: false,
default: () => {},
},
theme: { type: Object, required: false, default: null },
themeConfigBlockType: {
type: Object,
required: true,
},
element: {
type: Object,
required: false,
default: null,
},
preview: {
type: Boolean,
required: false,
default: true,
},
},
}
</script>

View file

@ -0,0 +1,39 @@
<template>
<Expandable default-expanded>
<template #header="{ toggle, expanded }">
<a v-if="title" class="theme-config-block__title" @click="toggle">
{{ title }}
<i
class="theme-config-block__title-icon"
:class="{
'iconoir-nav-arrow-down': expanded,
'iconoir-nav-arrow-right': !expanded,
}"
/>
</a>
</template>
<template #default>
<div class="theme-config-block-section">
<div class="theme-config-block-section__properties">
<slot></slot>
</div>
<div class="theme-config-block-section__preview">
<slot name="preview"></slot>
</div>
</div>
</template>
</Expandable>
</template>
<script>
export default {
name: 'ThemeConfigBlockSection',
props: {
title: {
type: String,
required: false,
default: null,
},
},
}
</script>

View file

@ -5,30 +5,19 @@
</template>
<script>
import { ThemeConfigBlockType } from '@baserow/modules/builder/themeConfigBlockTypes'
export default {
name: 'ThemeProvider',
inject: ['builder'],
computed: {
themeConfigBlocks() {
return this.$registry.getOrderedList('themeConfigBlock')
},
style() {
const colors = {
'--primary-color': this.builder.theme.primary_color,
'--secondary-color': this.builder.theme.secondary_color,
}
const buttonColors = {
'--button-color': this.builder.theme.primary_color,
}
const headings = Array.from([1, 2, 3, 4, 5, 6]).reduce(
(headings, level) => ({
[`--heading-h${level}-font-size`]: `${
this.builder.theme[`heading_${level}_font_size`]
}px`,
[`--heading-h${level}-color`]:
this.builder.theme[`heading_${level}_color`],
...headings,
}),
{}
return ThemeConfigBlockType.getAllStyles(
this.themeConfigBlocks,
this.builder.theme
)
return { ...colors, ...headings, ...buttonColors }
},
},
}

View file

@ -0,0 +1,115 @@
<template>
<div>
<ThemeConfigBlockSection
v-for="i in headings"
:key="i"
:title="$t('typographyThemeConfigBlock.headingLabel', { i })"
>
<template #default>
<FormGroup horizontal :label="$t('typographyThemeConfigBlock.size')">
<div
class="input__with-icon heading-theme-config-block__input-number"
>
<input
v-model="values[`heading_${i}_font_size`]"
type="number"
class="input remove-number-input-controls input--small"
:min="fontSizeMin"
:max="fontSizeMax"
:class="{
'input--error': $v.values[`heading_${i}_font_size`].$invalid,
}"
@blur="$v.values[`heading_${i}_font_size`].$touch()"
/>
<i>px</i>
</div>
<template #after-input>
<ResetButton
v-model="values"
:theme="theme"
:property="`heading_${i}_font_size`"
/>
</template>
</FormGroup>
<FormGroup horizontal :label="$t('typographyThemeConfigBlock.color')">
<ColorInput
v-model="values[`heading_${i}_text_color`]"
:color-variables="colorVariables"
:default-value="theme ? theme[`heading_${i}_text_color`] : null"
small
/>
<template #after-input>
<ResetButton
v-model="values"
:theme="theme"
:property="`heading_${i}_text_color`"
/>
</template>
</FormGroup>
</template>
<template #preview>
<component
:is="`h${i}`"
class="margin-bottom-2 theme-settings__section-ellipsis"
:class="`ab-heading--h${i}`"
>
{{ $t('typographyThemeConfigBlock.headingValue', { i }) }}
</component>
</template>
</ThemeConfigBlockSection>
</div>
</template>
<script>
import { required, integer, minValue, maxValue } from 'vuelidate/lib/validators'
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'
const fontSizeMin = 1
const fontSizeMax = 100
const headings = [1, 2, 3]
export default {
name: 'TypographyThemeConfigBlock',
components: { ThemeConfigBlockSection, ResetButton },
mixins: [themeConfigBlock],
data() {
return {
values: {},
allowedValues: headings
.map((level) => [
`heading_${level}_text_color`,
`heading_${level}_font_size`,
])
.flat(),
}
},
computed: {
headings() {
if (this.element?.level) {
return [this.element.level]
} else {
return headings
}
},
fontSizeMin() {
return fontSizeMin
},
fontSizeMax() {
return fontSizeMax
},
},
validations: {
values: headings.reduce((o, i) => {
o[`heading_${i}_font_size`] = {
required,
integer,
minValue: minValue(fontSizeMin),
maxValue: maxValue(fontSizeMax),
}
return o
}, {}),
},
}
</script>

View file

@ -381,18 +381,36 @@
"borderLabel": "Border",
"paddingLabel": "Padding"
},
"mainThemeConfigBlock": {
"colorsLabel": "Colors",
"themeConfigBlockType": {
"color": "Colors",
"typography": "Typography",
"button": "Button"
},
"colorThemeConfigBlock": {
"primaryColor": "Primary",
"secondaryColor": "Secondary",
"typography": "Typography",
"borderColor": "Border"
},
"colorThemeConfigBlockType": {
"primary": "Primary",
"secondary": "Secondary",
"border": "Border"
},
"typographyThemeConfigBlock": {
"headingLabel": "Heading {i} (h{i})",
"headingValue": "Heading <h{i}>"
"headingValue": "Heading <h{i}>",
"color": "Color",
"size": "Size"
},
"buttonThemeConfigBlock": {
"backgroundColor": "Background color",
"button": "Button",
"defaultState": "Default state",
"hoverState": "Hover state"
},
"buttonElementForm": {
"valueLabel": "Text",
"valuePlaceholder": "Enter text...",
"buttonColor": "Button color"
"valueLabel": "Button text",
"valuePlaceholder": "Enter text..."
},
"buttonElement": {
"noValue": "Unnamed..."
@ -605,5 +623,8 @@
"buttonFieldForm": {
"infoMessage": "To configure actions for this button, open the \"Events\" tab of the current element.",
"labelPlaceholder": "Enter a label..."
},
"resetButton": {
"reset": "Reset to default theme value"
}
}

View file

@ -1,8 +1,8 @@
import RuntimeFormulaContext from '@baserow/modules/core/runtimeFormulaContext'
import { resolveFormula } from '@baserow/modules/core/formula'
import { resolveColor } from '@baserow/modules/core/utils/colors'
import { themeToColorVariables } from '@baserow/modules/builder/utils/theme'
import applicationContextMixin from '@baserow/modules/builder/mixins/applicationContext'
import { ThemeConfigBlockType } from '@baserow/modules/builder/themeConfigBlockTypes'
export default {
inject: ['workspace', 'builder', 'page', 'mode'],
@ -59,8 +59,14 @@ export default {
},
}
},
themeConfigBlocks() {
return this.$registry.getOrderedList('themeConfigBlock')
},
colorVariables() {
return themeToColorVariables(this.builder.theme)
return ThemeConfigBlockType.getAllColorVariables(
this.themeConfigBlocks,
this.builder.theme
)
},
},
methods: {
@ -115,7 +121,13 @@ export default {
}
}
},
getStyleOverride(key, colorVariables = null) {
return ThemeConfigBlockType.getAllStyles(
this.themeConfigBlocks,
this.element.styles[key] || {},
colorVariables || this.colorVariables
)
},
resolveColor,
},
}

View file

@ -1,5 +1,5 @@
import { resolveColor } from '@baserow/modules/core/utils/colors'
import { themeToColorVariables } from '@baserow/modules/builder/utils/theme'
import { ThemeConfigBlockType } from '@baserow/modules/builder/themeConfigBlockTypes'
import form from '@baserow/modules/core/mixins/form'
import {
DATA_PROVIDERS_ALLOWED_ELEMENTS,
@ -10,10 +10,15 @@ export default {
inject: ['workspace', 'builder', 'page', 'mode'],
mixins: [form],
computed: {
colorVariables() {
return themeToColorVariables(this.builder.theme)
themeConfigBlocks() {
return this.$registry.getOrderedList('themeConfigBlock')
},
colorVariables() {
return ThemeConfigBlockType.getAllColorVariables(
this.themeConfigBlocks,
this.builder.theme
)
},
DATA_PROVIDERS_ALLOWED_ELEMENTS: () => DATA_PROVIDERS_ALLOWED_ELEMENTS,
DATA_PROVIDERS_ALLOWED_FORM_ELEMENTS: () =>
DATA_PROVIDERS_ALLOWED_FORM_ELEMENTS,

View file

@ -1,20 +0,0 @@
import { resolveColor } from '@baserow/modules/core/utils/colors'
import { themeToColorVariables } from '@baserow/modules/builder/utils/theme'
export default {
computed: {
headingColorVariables() {
const level = this.values?.level || this.element.level
const variables = themeToColorVariables(this.builder.theme)
variables.unshift({
name: `H${level} default`,
value: 'default',
color: this.builder.theme[`heading_${level}_color`],
})
return variables
},
},
methods: {
resolveColor,
},
}

View file

@ -0,0 +1,82 @@
import { ThemeConfigBlockType } from '@baserow/modules/builder/themeConfigBlockTypes'
import form from '@baserow/modules/core/mixins/form'
import { clone } from '@baserow/modules/core/utils/object'
import _ from 'lodash'
export default {
inject: ['builder'],
mixins: [form],
props: {
preview: {
type: Boolean,
required: false,
default: true,
},
element: {
type: Object,
required: false,
default: null,
},
theme: { type: Object, required: false, default: null },
},
data() {
return { values: {} }
},
computed: {
themeConfigBlocks() {
return this.$registry.getOrderedList('themeConfigBlock')
},
colorVariables() {
return ThemeConfigBlockType.getAllColorVariables(
this.themeConfigBlocks,
this.builder.theme
)
},
},
methods: {
emitChange(newValues) {
if (this.isFormValid()) {
const updated = { ...this.defaultValues, ...newValues }
const reference = this.theme ? this.theme : this.defaultValues
// We remove values that are equals to theme values to react on theme change
// if there is no value
const differences = Object.fromEntries(
Object.entries(updated).filter(
([key, value]) => !_.isEqual(value, reference[key])
)
)
this.$emit('values-changed', differences)
}
},
// Overrides form mixin getDefaultValues to merge theme values with existing values
getDefaultValues() {
if (this.allowedValues === null) {
if (this.theme) {
return { ...clone(this.theme), ...clone(this.defaultValues) }
} else {
return clone(this.defaultValues)
}
}
const mergedValues = { ...this.theme, ...this.defaultValues }
return Object.keys(mergedValues).reduce((result, key) => {
if (this.allowedValues.includes(key)) {
let value = mergedValues[key]
// If the value is an array or object, it could be that it contains
// references and we actually need a copy of the value here so that we don't
// directly change existing variables when editing form values.
if (
Array.isArray(value) ||
(typeof value === 'object' && value !== null)
) {
value = clone(value)
}
result[key] = value
}
return result
}, {})
},
},
}

View file

@ -84,7 +84,11 @@ import {
UserDataProviderType,
} from '@baserow/modules/builder/dataProviderTypes'
import { MainThemeConfigBlock } from '@baserow/modules/builder/themeConfigBlockTypes'
import {
ColorThemeConfigBlockType,
TypographyThemeConfigBlockType,
ButtonThemeConfigBlockType,
} from '@baserow/modules/builder/themeConfigBlockTypes'
import {
CreateRowWorkflowActionType,
NotificationWorkflowActionType,
@ -241,7 +245,18 @@ export default (context) => {
'builderDataProvider',
new PreviousActionDataProviderType(context)
)
app.$registry.register('themeConfigBlock', new MainThemeConfigBlock(context))
app.$registry.register(
'themeConfigBlock',
new ColorThemeConfigBlockType(context)
)
app.$registry.register(
'themeConfigBlock',
new TypographyThemeConfigBlockType(context)
)
app.$registry.register(
'themeConfigBlock',
new ButtonThemeConfigBlockType(context)
)
app.$registry.register(
'workflowAction',

View file

@ -1,22 +1,210 @@
import { Registerable } from '@baserow/modules/core/registry'
import MainThemeConfigBlockComponent from '@baserow/modules/builder/components/theme/MainThemeConfigBlock'
import ColorThemeConfigBlock from '@baserow/modules/builder/components/theme/ColorThemeConfigBlock'
import TypographyThemeConfigBlock from '@baserow/modules/builder/components/theme/TypographyThemeConfigBlock'
import ButtonThemeConfigBlock from '@baserow/modules/builder/components/theme/ButtonThemeConfigBlock'
import { resolveColor } from '@baserow/modules/core/utils/colors'
/**
* Helper class to construct easily style objects.
*/
export class ThemeStyle {
constructor() {
this.style = {}
}
addIfExists(theme, propName, styleName, transform = (v) => v) {
if (Object.prototype.hasOwnProperty.call(theme, propName)) {
this.style[styleName] = transform(theme[propName])
}
}
toObject() {
return this.style
}
}
export class ThemeConfigBlockType extends Registerable {
static getType() {
get label() {
return null
}
/**
* Return the CSS to apply for the theme config block. Essentially it's mainly
* definitions of a bunch of CSS vars that are used by the ABComponents to style
* themselves.
*
* @param {Object} theme the theme to use to populate the CSS vars.
* @param {Array} colorVariables the color variable mapping.
* @returns an object that can be use as style property for a DOM element
*/
getCSS(theme, colorVariables) {
return null
}
/**
* Returns the color variables provided by this theme config block. These variables
* can then be used by other theme block properties.
*
* @param {Object} theme the theme object that contains the value definitions.
* @returns An array of color variables.
*/
getColorVariables(theme) {
return []
}
/**
* Returns the component to configure this theme block.
*/
get component() {
return null
}
/**
* Iterate over the registered theme blocks to generate the full CSS style object.
* This object can then be used to style the application.
*
* @param {Array} themeBlocks the list of themeBlocks to consider.
* @param {Object} theme the theme of the application.
* @param {Array} colorVariables the color variables array.
* @returns
*/
static getAllStyles(themeBlocks, theme, colorVariables = null) {
if (colorVariables === null) {
colorVariables = this.getAllColorVariables(themeBlocks, theme)
}
return themeBlocks
.map((block) => block.getCSS(theme, colorVariables))
.reduce((acc, obj) => ({ ...acc, ...obj }), {})
}
/**
* Get all variables from all theme blocks.
*
* @param {Array} themeBlocks theme blocks to consider
* @param {Object} theme theme of the application
* @returns the color variables array
*/
static getAllColorVariables(themeBlocks, theme) {
return themeBlocks.map((block) => block.getColorVariables(theme)).flat()
}
getOrder() {
return 50
}
}
export class MainThemeConfigBlock extends ThemeConfigBlockType {
export class ColorThemeConfigBlockType extends ThemeConfigBlockType {
static getType() {
return 'main'
return 'color'
}
get label() {
return this.app.i18n.t('themeConfigBlockType.color')
}
getCSS(theme, colorVariables) {
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()
}
getColorVariables(theme) {
const { i18n } = this.app
return [
{
name: i18n.t('colorThemeConfigBlockType.primary'),
value: 'primary',
color: theme.primary_color,
},
{
name: i18n.t('colorThemeConfigBlockType.secondary'),
value: 'secondary',
color: theme.secondary_color,
},
{
name: i18n.t('colorThemeConfigBlockType.border'),
value: 'border',
color: theme.border_color,
},
]
}
get component() {
return MainThemeConfigBlockComponent
return ColorThemeConfigBlock
}
getOrder() {
return 10
}
}
export class TypographyThemeConfigBlockType extends ThemeConfigBlockType {
static getType() {
return 'typography'
}
get label() {
return this.app.i18n.t('themeConfigBlockType.typography')
}
getCSS(theme, colorVariables) {
const style = new ThemeStyle()
Array.from([1, 2, 3, 4, 5, 6]).forEach((level) => {
style.addIfExists(
theme,
`heading_${level}_font_size`,
`--heading-h${level}-font-size`,
(v) => `${Math.min(100, v)}px`
)
style.addIfExists(
theme,
`heading_${level}_text_color`,
`--heading-h${level}-color`,
(v) => resolveColor(v, colorVariables)
)
})
return style.toObject()
}
get component() {
return TypographyThemeConfigBlock
}
getOrder() {
return 20
}
}
export class ButtonThemeConfigBlockType extends ThemeConfigBlockType {
static getType() {
return 'button'
}
get label() {
return this.app.i18n.t('themeConfigBlockType.button')
}
getCSS(theme, colorVariables) {
const style = new ThemeStyle()
style.addIfExists(theme, 'button_background_color', '--button-color', (v) =>
resolveColor(v, colorVariables)
)
style.addIfExists(
theme,
'button_hover_background_color',
'--hover-button-color',
(v) => resolveColor(v, colorVariables)
)
return style.toObject()
}
get component() {
return ButtonThemeConfigBlock
}
getOrder() {
return 40
}
}

View file

@ -0,0 +1,8 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.7854 15.75V21.75" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.7856 2.25V4.85" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.21436 8.25V2.25" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.21436 21.7499V19.1499" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.7858 11.3501C19.561 11.3501 21.0001 9.89502 21.0001 8.1001C21.0001 6.30517 19.561 4.8501 17.7858 4.8501C16.0106 4.8501 14.5715 6.30517 14.5715 8.1001C14.5715 9.89502 16.0106 11.3501 17.7858 11.3501Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.21429 19.15C7.98949 19.15 9.42857 17.6949 9.42857 15.9C9.42857 14.1051 7.98949 12.65 6.21429 12.65C4.43908 12.65 3 14.1051 3 15.9C3 17.6949 4.43908 19.15 6.21429 19.15Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

(image error) Size: 1.1 KiB

View file

@ -155,7 +155,6 @@
@import 'color_input_group';
@import 'formula_input_field';
@import 'get_formula_component';
@import 'theme_settings';
@import 'color_input';
@import 'group_bys';
@import 'data_explorer/node';

View file

@ -1,4 +1,5 @@
@import 'elements/all';
@import 'theme/all';
@import 'page_builder';
@import 'elements_context';
@import 'add_element_card';

View file

@ -50,8 +50,9 @@
text-align: right;
}
&:not(.loading-spinner):hover {
filter: brightness(1.3);
&:not(.loading-spinner):hover,
&:not(.loading-spinner).ab-button--force-hover {
background-color: var(--hover-button-color, $black);
text-decoration: none;
}

View file

@ -0,0 +1,5 @@
@import 'heading_theme_config_block';
@import 'theme_config_block';
@import 'theme_config_block_section';
@import 'custom_style';
@import 'theme_settings';

View file

@ -0,0 +1,16 @@
.custom-style {
height: 2px;
}
.custom-style__button {
float: right;
}
.custom-style__config-blocks {
width: 350px;
padding: 20px 20px 5px;
}
.custom-style__config-block-title {
padding: 0 10px;
}

View file

@ -0,0 +1,9 @@
.heading-theme-config-block__inputs {
display: flex;
align-items: center;
gap: 5px;
}
.heading-theme-config-block__input-number {
width: 80px;
}

View file

@ -0,0 +1,44 @@
.theme-config-block__title {
color: $color-neutral-900;
font-size: 14px;
font-weight: bold;
display: flex;
place-content: center space-between;
margin-bottom: 15px;
width: 50%;
user-select: none;
&:hover {
text-decoration: none;
}
.theme-config-block--no-preview & {
width: 100%;
}
}
.theme-config-block__title-icon {
font-size: 12px;
}
.theme-config-block {
& .control--horizontal {
flex-wrap: nowrap;
.control__elements {
flex-basis: 50%;
}
.control__label {
flex-basis: 50%;
color: $color-neutral-700;
}
}
&:not(.theme-config-block--no-preview)::after {
@include absolute(0, calc(50% - 14px), 0, auto);
content: '';
border-right: 1px solid $color-neutral-100;
}
}

View file

@ -0,0 +1,22 @@
.theme-config-block-section {
display: flex;
width: 100%;
gap: 28px;
}
.theme-config-block-section__properties {
flex: 50% 0 0;
.theme-config-block--no-preview & {
flex: 100% 0 0;
}
}
.theme-config-block-section__preview {
flex: 50% 0 0;
width: 50%;
.theme-config-block--no-preview & {
display: none;
}
}

View file

@ -0,0 +1,6 @@
.theme-settings {
position: relative;
height: 100%;
padding-top: 10px;
padding-bottom: 10px;
}

View file

@ -1,11 +1,45 @@
.color-input__preview {
display: block;
width: 40px;
height: 20px;
width: 16px;
height: 16px;
border-radius: 2px;
box-shadow: 1px 1px 1px 1px rgba($black, 0.2);
border: 1px solid $palette-neutral-400;
}
&:hover {
box-shadow: 1px 1px 1px 0 rgba($black, 0.2);
.color-input__input {
width: 100%;
border: 1px solid $palette-neutral-400;
padding: 0 12px;
outline: none;
height: 44px;
box-sizing: border-box;
display: flex;
justify-content: start;
align-items: center;
gap: 10px;
font-weight: 400;
font-size: 13px;
user-select: none;
min-width: 120px;
font-family: Inter, sans-serif;
cursor: pointer;
@include rounded($rounded-md);
@include elevation($elevation-low);
&:active,
&:focus {
border-color: $palette-blue-500;
}
.color-input--small & {
height: 36px;
padding: 8px 12px;
}
&[disabled] {
color: $palette-neutral-700;
box-shadow: none;
background-color: $palette-neutral-100;
}
}

View file

@ -1,49 +0,0 @@
.theme-settings {
position: relative;
height: 100%;
padding: 32px 0;
&::after {
@include absolute(0, 50%, 0, auto);
content: '';
border-right: 1px solid $color-neutral-200;
}
}
.theme-settings__section {
display: flex;
width: 100%;
}
.theme-settings__section-properties {
flex: 50% 0 0;
padding: 0 20px;
}
.theme-settings__section-title {
color: $color-neutral-900;
font-size: 14px;
font-weight: bold;
display: flex;
place-content: center space-between;
margin-bottom: 20px;
&:hover {
text-decoration: none;
}
}
.theme-settings__section-title-icon {
font-size: 12px;
}
.theme-settings__section-preview {
flex: 50% 0 0;
width: 50%;
padding: 0 32px;
}
.theme-settings__section-ellipsis {
@extend %ellipsis;
}

View file

@ -76,4 +76,4 @@ $baserow-icons: 'circle-empty', 'circle-checked', 'check-square', 'formula',
'file-audio', 'file-video', 'file-code', 'tablet', 'form', 'file-excel',
'kanban', 'file-word', 'file-archive', 'gallery', 'file-powerpoint',
'calendar', 'smile', 'smartphone', 'plus', 'heading-1', 'heading-2',
'heading-3', 'paragraph', 'ordered-list', 'enlarge';
'heading-3', 'paragraph', 'ordered-list', 'enlarge', 'settings';

View file

@ -1,19 +1,25 @@
<template>
<div>
<div :class="{ 'color-input--small': small }">
<ColorPickerContext
ref="colorPicker"
:value="value"
:variables="colorVariables"
:variables="localColorVariables"
@input="$emit('input', $event)"
/>
<a
<div
ref="opener"
class="color-input__preview"
:style="{
'background-color': resolveColor(value, colorVariables),
}"
class="color-input__input"
tabindex="0"
@click="$refs.colorPicker.toggle($refs.opener)"
/>
>
<span
class="color-input__preview"
:style="{
'background-color': actualValue,
}"
/>
<span>{{ displayValue }}</span>
</div>
</div>
</template>
@ -35,6 +41,50 @@ export default {
required: false,
default: () => [],
},
small: {
type: Boolean,
required: false,
default: false,
},
defaultValue: {
type: String,
required: false,
default: null,
},
},
computed: {
variablesMap() {
return Object.fromEntries(
this.localColorVariables.map((v) => [v.value, v])
)
},
localColorVariables() {
if (this.defaultValue) {
return [
{
value: this.defaultValue,
color: resolveColor(this.defaultValue, this.colorVariables),
name: this.$t('colorInput.default'),
},
...this.colorVariables,
]
} else {
return this.colorVariables
}
},
displayValue() {
const found = this.localColorVariables.find(
({ value }) => value === this.value
)
if (found) {
return found.name
} else {
return this.value.toUpperCase()
}
},
actualValue() {
return resolveColor(this.value, this.variablesMap)
},
},
methods: { resolveColor },
}

View file

@ -2,7 +2,7 @@
<Context class="color-picker-context">
<ColorPicker
:value="hexColorIncludingAlpha"
@input=";[colorUpdated($event), $emit('input', $event)]"
@input="setColorFromPicker($event)"
></ColorPicker>
<div class="color-picker-context__color">
<Dropdown
@ -66,21 +66,25 @@
v-if="Object.keys(variables).length > 0"
class="color-picker-context__variables"
>
<Dropdown :value="isVariable ? value : ''" small @input="setVariable">
<Dropdown
:value="selectedVariable?.name || ''"
small
@input="setVariable"
>
<DropdownItem name="Custom" value=""></DropdownItem>
<DropdownItem
v-for="variable in variables"
:key="variable.name"
:name="variable.name"
:value="variable.value"
:value="variable.name"
>
<div
class="color-picker-context__variable-color"
:style="{ 'background-color': variable.color }"
></div>
<span class="select__item-name-text" :title="variable.name">{{
variable.name
}}</span>
<span class="select__item-name-text" :title="variable.name">
{{ variable.name }}
</span>
</DropdownItem>
</Dropdown>
</div>
@ -91,7 +95,6 @@
import context from '@baserow/modules/core/mixins/context'
import ColorPicker from '@baserow/modules/core/components/ColorPicker.vue'
import {
isColorVariable,
isValidHexColor,
convertHexToRgb,
convertRgbToHex,
@ -132,9 +135,8 @@ export default {
},
computed: {
COLOR_NOTATIONS: () => COLOR_NOTATIONS,
isVariable() {
const variableValues = this.variables.map((v) => v.value)
return isColorVariable(this.value) && variableValues.includes(this.value)
selectedVariable() {
return this.variables.find(({ value }) => value === this.value)
},
},
watch: {
@ -143,8 +145,11 @@ export default {
// Only update the value if it has actually changed because otherwise the user's
// input can be overwritten from converting to and from hex values.
if (value !== oldValue) {
if (this.isVariable) {
this.setVariable(value)
const variable = this.variables.find(
(variable) => variable.value === value
)
if (variable !== undefined) {
this.colorUpdated(variable.color)
} else {
this.colorUpdated(value)
}
@ -154,6 +159,15 @@ export default {
},
},
methods: {
setColorFromPicker(value) {
this.colorUpdated(value)
this.$emit(
'input',
this.a === 100
? this.hexColorExcludingAlpha
: this.hexColorIncludingAlpha
)
},
/**
* Called whenever the original value is updated. It will make sure that all the
* variables are updated accordingly if the value is a valid hex color.
@ -206,6 +220,9 @@ export default {
rgba.r = newRgba.r
rgba.g = newRgba.g
rgba.b = newRgba.b
if (rgba.a === 1) {
delete rgba.a
}
const hex = convertRgbToHex(rgba)
this.colorUpdated(hex)
@ -220,7 +237,7 @@ export default {
*/
setVariable(value) {
const variable = this.variables.find(
(variable) => variable.value === value
(variable) => variable.name === value
)
if (variable !== undefined) {
this.colorUpdated(variable.color)

File diff suppressed because it is too large Load diff

View file

@ -2132,6 +2132,7 @@
/>
</div>
<div
id="colorPicker"
class="margin-bottom-3"
style="background-color: #ffffff; padding: 20px"
>
@ -2140,8 +2141,9 @@
<a
ref="colorPickerLink"
@click="$refs.colorPicker.toggle($refs.colorPickerLink)"
>Open color picker context</a
>
Open color picker context
</a>
<ColorPickerContext
ref="colorPicker"
v-model="color"
@ -2149,14 +2151,10 @@
></ColorPickerContext>
<br /><br />
{{ color }} - {{ resolveColor(color, colorVariables) }} <br /><br />
<div
:style="{
width: '40px',
height: '20px',
'background-color': resolveColor(color, colorVariables),
}"
></div>
<br />
<ColorInput v-model="color" :color-variables="colorVariables" />
</div>
<div class="margin-bottom-3">
<h2>Call to action</h2>
<CallToAction> My call to action. Click me! </CallToAction>

View file

@ -227,11 +227,20 @@ export function isColorVariable(value) {
return value.substring(0, 1) !== '#'
}
export function resolveColor(value, variables) {
if (isColorVariable(value)) {
const variable = variables.find((v) => v.value === value)
if (variable !== undefined) {
return variable.color
export function resolveColor(value, variables, recursively = true) {
let varMap = variables
if (Array.isArray(varMap)) {
varMap = Object.fromEntries(variables.map((v) => [v.value, v]))
}
if (varMap[value]) {
if (recursively) {
return resolveColor(varMap[value].color, {
...varMap,
[value]: undefined,
})
} else {
return varMap[value].color
}
}

View file

@ -14,5 +14,10 @@ describe('colorUtils', () => {
{ name: 'Primary', value: 'primary', color: '#ff000000' },
])
).toBe('secondary')
expect(
resolveColor('#00000042', [
{ name: 'Default', value: '#00000042', color: '#00000042' },
])
).toBe('#00000042')
})
})