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:
parent
c352726f0a
commit
495af48ede
73 changed files with 2632 additions and 1344 deletions
backend
src/baserow
tests/baserow
contrib
builder
integrations/local_baserow
core/user_sources
changelog/entries/unreleased/feature
web-frontend
modules
builder
components
elements/components
ButtonElement.vueFormContainerElement.vueHeadingElement.vueLinkElement.vueTableElement.vue
collectionField/form
forms
settings
theme
locales
mixins
plugin.jsthemeConfigBlockTypes.jscore
assets
icons
scss
components
locales
pages
utils
test/unit/core/utils
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
),
|
||||
]
|
|
@ -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",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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",
|
||||
)
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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": [],
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "feature",
|
||||
"message": "[Builder] Improved application styling capabilities",
|
||||
"issue_number": 2388,
|
||||
"bullet_points": [],
|
||||
"created_at": "2024-06-07"
|
||||
}
|
|
@ -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))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
<template>
|
||||
<div
|
||||
:style="{
|
||||
'--button-color': resolveColor(element.button_color, colorVariables),
|
||||
}"
|
||||
>
|
||||
<div :style="getStyleOverride('button')">
|
||||
<div
|
||||
v-if="
|
||||
mode === 'editing' &&
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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'
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
],
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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'],
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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'],
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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') }}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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')
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 }
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
82
web-frontend/modules/builder/mixins/themeConfigBlock.js
Normal file
82
web-frontend/modules/builder/mixins/themeConfigBlock.js
Normal 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
|
||||
}, {})
|
||||
},
|
||||
},
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
8
web-frontend/modules/core/assets/icons/settings.svg
Normal file
8
web-frontend/modules/core/assets/icons/settings.svg
Normal 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 |
|
@ -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';
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
@import 'elements/all';
|
||||
@import 'theme/all';
|
||||
@import 'page_builder';
|
||||
@import 'elements_context';
|
||||
@import 'add_element_card';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
@import 'heading_theme_config_block';
|
||||
@import 'theme_config_block';
|
||||
@import 'theme_config_block_section';
|
||||
@import 'custom_style';
|
||||
@import 'theme_settings';
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
.heading-theme-config-block__inputs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.heading-theme-config-block__input-number {
|
||||
width: 80px;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
.theme-settings {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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 },
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Add table
Reference in a new issue