mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-17 18:32:35 +00:00
Configure Link decoration in Builder Theme
This commit is contained in:
parent
7cec473987
commit
722bd54323
22 changed files with 760 additions and 53 deletions
backend
changelog/entries/unreleased/feature
web-frontend/modules
builder
core
|
@ -37,16 +37,37 @@ class DynamicConfigBlockSerializer(serializers.Serializer):
|
||||||
if not isinstance(theme_config_block_type_name, list):
|
if not isinstance(theme_config_block_type_name, list):
|
||||||
theme_config_block_type_name = [theme_config_block_type_name]
|
theme_config_block_type_name = [theme_config_block_type_name]
|
||||||
|
|
||||||
for prop, type_name in zip(property_name, theme_config_block_type_name):
|
for prop, type_names in zip(property_name, theme_config_block_type_name):
|
||||||
theme_config_block_type = theme_config_block_registry.get(type_name)
|
if not isinstance(type_names, list):
|
||||||
self.fields[prop] = theme_config_block_type.get_serializer_class(
|
type_names = [type_names]
|
||||||
request_serializer=request_serializer
|
|
||||||
)(**({"help_text": f"Styles overrides for {prop}"} | serializer_kwargs))
|
config_blocks = (
|
||||||
|
theme_config_block_registry.get(type_name) for type_name in type_names
|
||||||
|
)
|
||||||
|
serializer_class = combine_theme_config_blocks_serializer_class(
|
||||||
|
config_blocks,
|
||||||
|
request_serializer=request_serializer,
|
||||||
|
name="SubConfigBlockSerializer",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.fields[prop] = serializer_class(**serializer_kwargs)
|
||||||
|
|
||||||
|
all_type_names = "".join(
|
||||||
|
[
|
||||||
|
"And".join(sub.capitalize() for sub in p)
|
||||||
|
if isinstance(p, list)
|
||||||
|
else p.capitalize()
|
||||||
|
for p in theme_config_block_type_name
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# Dynamically create the Meta class with ref name to prevent collision
|
# Dynamically create the Meta class with ref name to prevent collision
|
||||||
class DynamicMeta:
|
class DynamicMeta:
|
||||||
type_names = "".join([p.capitalize() for p in theme_config_block_type_name])
|
type_names = all_type_names
|
||||||
ref_name = f"{type_names}ConfigBlockSerializer"
|
ref_name = f"{all_type_names}ConfigBlockSerializer"
|
||||||
|
meta_ref_name = f"{all_type_names}ConfigBlockSerializer"
|
||||||
|
|
||||||
|
print("Serializer for ", f"{all_type_names}ConfigBlockSerializer")
|
||||||
|
|
||||||
self.Meta = DynamicMeta
|
self.Meta = DynamicMeta
|
||||||
|
|
||||||
|
@ -72,6 +93,41 @@ def serialize_builder_theme(builder: Builder) -> dict:
|
||||||
return theme
|
return theme
|
||||||
|
|
||||||
|
|
||||||
|
def combine_theme_config_blocks_serializer_class(
|
||||||
|
theme_config_blocks,
|
||||||
|
request_serializer=False,
|
||||||
|
name="CombinedThemeConfigBlocksSerializer",
|
||||||
|
) -> serializers.Serializer:
|
||||||
|
"""
|
||||||
|
This helper function generates one single serializer that contains all the fields
|
||||||
|
of all the theme config blocks. The API always communicates all theme properties
|
||||||
|
flat in one single object.
|
||||||
|
|
||||||
|
:return: The generated serializer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
attrs = {}
|
||||||
|
|
||||||
|
for theme_config_block in theme_config_blocks:
|
||||||
|
serializer = theme_config_block.get_serializer_class(
|
||||||
|
request_serializer=request_serializer
|
||||||
|
)
|
||||||
|
serializer_fields = serializer().get_fields()
|
||||||
|
|
||||||
|
for name, field in serializer_fields.items():
|
||||||
|
attrs[name] = field
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ref_name = "".join(t.type.capitalize() for t in theme_config_blocks) + name
|
||||||
|
meta_ref_name = "".join(t.type.capitalize() for t in theme_config_blocks) + name
|
||||||
|
|
||||||
|
attrs["Meta"] = Meta
|
||||||
|
|
||||||
|
class_object = type(name, (serializers.Serializer,), attrs)
|
||||||
|
|
||||||
|
return class_object
|
||||||
|
|
||||||
|
|
||||||
@cache
|
@cache
|
||||||
def get_combined_theme_config_blocks_serializer_class(
|
def get_combined_theme_config_blocks_serializer_class(
|
||||||
request_serializer=False,
|
request_serializer=False,
|
||||||
|
@ -90,28 +146,10 @@ def get_combined_theme_config_blocks_serializer_class(
|
||||||
"imported before the theme config blocks have been registered."
|
"imported before the theme config blocks have been registered."
|
||||||
)
|
)
|
||||||
|
|
||||||
attrs = {}
|
return combine_theme_config_blocks_serializer_class(
|
||||||
|
theme_config_block_registry.get_all(), request_serializer=request_serializer
|
||||||
for theme_config_block in theme_config_block_registry.get_all():
|
|
||||||
serializer = theme_config_block.get_serializer_class(
|
|
||||||
request_serializer=request_serializer
|
|
||||||
)
|
|
||||||
serializer_fields = serializer().get_fields()
|
|
||||||
|
|
||||||
for name, field in serializer_fields.items():
|
|
||||||
attrs[name] = field
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
meta_ref_name = "combined_theme_config_blocks_serializer"
|
|
||||||
|
|
||||||
attrs["Meta"] = Meta
|
|
||||||
|
|
||||||
class_object = type(
|
|
||||||
"CombinedThemeConfigBlocksSerializer", (serializers.Serializer,), attrs
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return class_object
|
|
||||||
|
|
||||||
|
|
||||||
CombinedThemeConfigBlocksSerializer = (
|
CombinedThemeConfigBlocksSerializer = (
|
||||||
get_combined_theme_config_blocks_serializer_class()
|
get_combined_theme_config_blocks_serializer_class()
|
||||||
|
|
|
@ -257,7 +257,6 @@ class BuilderConfig(AppConfig):
|
||||||
ImageThemeConfigBlockType,
|
ImageThemeConfigBlockType,
|
||||||
InputThemeConfigBlockType,
|
InputThemeConfigBlockType,
|
||||||
LinkThemeConfigBlockType,
|
LinkThemeConfigBlockType,
|
||||||
MenuThemeConfigBlockType,
|
|
||||||
PageThemeConfigBlockType,
|
PageThemeConfigBlockType,
|
||||||
TableThemeConfigBlockType,
|
TableThemeConfigBlockType,
|
||||||
TypographyThemeConfigBlockType,
|
TypographyThemeConfigBlockType,
|
||||||
|
@ -266,12 +265,11 @@ class BuilderConfig(AppConfig):
|
||||||
theme_config_block_registry.register(ColorThemeConfigBlockType())
|
theme_config_block_registry.register(ColorThemeConfigBlockType())
|
||||||
theme_config_block_registry.register(TypographyThemeConfigBlockType())
|
theme_config_block_registry.register(TypographyThemeConfigBlockType())
|
||||||
theme_config_block_registry.register(ButtonThemeConfigBlockType())
|
theme_config_block_registry.register(ButtonThemeConfigBlockType())
|
||||||
theme_config_block_registry.register(LinkThemeConfigBlockType())
|
|
||||||
theme_config_block_registry.register(ImageThemeConfigBlockType())
|
theme_config_block_registry.register(ImageThemeConfigBlockType())
|
||||||
theme_config_block_registry.register(PageThemeConfigBlockType())
|
theme_config_block_registry.register(PageThemeConfigBlockType())
|
||||||
theme_config_block_registry.register(InputThemeConfigBlockType())
|
theme_config_block_registry.register(InputThemeConfigBlockType())
|
||||||
theme_config_block_registry.register(TableThemeConfigBlockType())
|
theme_config_block_registry.register(TableThemeConfigBlockType())
|
||||||
theme_config_block_registry.register(MenuThemeConfigBlockType())
|
theme_config_block_registry.register(LinkThemeConfigBlockType())
|
||||||
|
|
||||||
from .workflow_actions.registries import builder_workflow_action_type_registry
|
from .workflow_actions.registries import builder_workflow_action_type_registry
|
||||||
from .workflow_actions.workflow_action_types import (
|
from .workflow_actions.workflow_action_types import (
|
||||||
|
|
|
@ -2011,7 +2011,8 @@ class MenuElementType(ElementType):
|
||||||
DynamicConfigBlockSerializer,
|
DynamicConfigBlockSerializer,
|
||||||
)
|
)
|
||||||
from baserow.contrib.builder.theme.theme_config_block_types import (
|
from baserow.contrib.builder.theme.theme_config_block_types import (
|
||||||
MenuThemeConfigBlockType,
|
ButtonThemeConfigBlockType,
|
||||||
|
LinkThemeConfigBlockType,
|
||||||
)
|
)
|
||||||
|
|
||||||
overrides = {
|
overrides = {
|
||||||
|
@ -2019,7 +2020,9 @@ class MenuElementType(ElementType):
|
||||||
"styles": DynamicConfigBlockSerializer(
|
"styles": DynamicConfigBlockSerializer(
|
||||||
required=False,
|
required=False,
|
||||||
property_name="menu",
|
property_name="menu",
|
||||||
theme_config_block_type_name=MenuThemeConfigBlockType.type,
|
theme_config_block_type_name=[
|
||||||
|
[ButtonThemeConfigBlockType.type, LinkThemeConfigBlockType.type]
|
||||||
|
],
|
||||||
serializer_kwargs={"required": False},
|
serializer_kwargs={"required": False},
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,116 @@
|
||||||
|
# Generated by Django 5.0.9 on 2025-03-18 13:03
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
import baserow.core.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("builder", "0054_simplecontainerelement"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="linkthemeconfigblock",
|
||||||
|
name="link_active_text_decoration",
|
||||||
|
field=baserow.core.fields.MultipleFlagField(
|
||||||
|
db_default="1000",
|
||||||
|
default="1000",
|
||||||
|
help_text="The text decoration flags [underline, strike, uppercase, italic]",
|
||||||
|
max_length=4,
|
||||||
|
num_flags=4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="linkthemeconfigblock",
|
||||||
|
name="link_default_text_decoration",
|
||||||
|
field=baserow.core.fields.MultipleFlagField(
|
||||||
|
db_default="1000",
|
||||||
|
default="1000",
|
||||||
|
help_text="The text decoration flags [underline, strike, uppercase, italic]",
|
||||||
|
max_length=4,
|
||||||
|
num_flags=4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="linkthemeconfigblock",
|
||||||
|
name="link_hover_text_decoration",
|
||||||
|
field=baserow.core.fields.MultipleFlagField(
|
||||||
|
db_default="1000",
|
||||||
|
default="1000",
|
||||||
|
help_text="The text decoration flags [underline, strike, uppercase, italic]",
|
||||||
|
max_length=4,
|
||||||
|
num_flags=4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="typographythemeconfigblock",
|
||||||
|
name="heading_1_text_decoration",
|
||||||
|
field=baserow.core.fields.MultipleFlagField(
|
||||||
|
db_default="0000",
|
||||||
|
default="0000",
|
||||||
|
help_text="The text decoration flags [underline, strike, uppercase, italic]",
|
||||||
|
max_length=4,
|
||||||
|
num_flags=4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="typographythemeconfigblock",
|
||||||
|
name="heading_2_text_decoration",
|
||||||
|
field=baserow.core.fields.MultipleFlagField(
|
||||||
|
db_default="0000",
|
||||||
|
default="0000",
|
||||||
|
help_text="The text decoration flags [underline, strike, uppercase, italic]",
|
||||||
|
max_length=4,
|
||||||
|
num_flags=4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="typographythemeconfigblock",
|
||||||
|
name="heading_3_text_decoration",
|
||||||
|
field=baserow.core.fields.MultipleFlagField(
|
||||||
|
db_default="0000",
|
||||||
|
default="0000",
|
||||||
|
help_text="The text decoration flags [underline, strike, uppercase, italic]",
|
||||||
|
max_length=4,
|
||||||
|
num_flags=4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="typographythemeconfigblock",
|
||||||
|
name="heading_4_text_decoration",
|
||||||
|
field=baserow.core.fields.MultipleFlagField(
|
||||||
|
db_default="0000",
|
||||||
|
default="0000",
|
||||||
|
help_text="The text decoration flags [underline, strike, uppercase, italic]",
|
||||||
|
max_length=4,
|
||||||
|
num_flags=4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="typographythemeconfigblock",
|
||||||
|
name="heading_5_text_decoration",
|
||||||
|
field=baserow.core.fields.MultipleFlagField(
|
||||||
|
db_default="0000",
|
||||||
|
default="0000",
|
||||||
|
help_text="The text decoration flags [underline, strike, uppercase, italic]",
|
||||||
|
max_length=4,
|
||||||
|
num_flags=4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="typographythemeconfigblock",
|
||||||
|
name="heading_6_text_decoration",
|
||||||
|
field=baserow.core.fields.MultipleFlagField(
|
||||||
|
db_default="0000",
|
||||||
|
default="0000",
|
||||||
|
help_text="The text decoration flags [underline, strike, uppercase, italic]",
|
||||||
|
max_length=4,
|
||||||
|
num_flags=4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name="MenuThemeConfigBlock",
|
||||||
|
),
|
||||||
|
]
|
|
@ -8,7 +8,7 @@ from baserow.contrib.builder.constants import (
|
||||||
FontWeights,
|
FontWeights,
|
||||||
HorizontalAlignments,
|
HorizontalAlignments,
|
||||||
)
|
)
|
||||||
from baserow.core.fields import AutoOneToOneField
|
from baserow.core.fields import AutoOneToOneField, MultipleFlagField
|
||||||
from baserow.core.user_files.models import UserFile
|
from baserow.core.user_files.models import UserFile
|
||||||
|
|
||||||
|
|
||||||
|
@ -84,6 +84,12 @@ class TypographyThemeConfigBlock(ThemeConfigBlock):
|
||||||
max_length=10,
|
max_length=10,
|
||||||
default=HorizontalAlignments.LEFT,
|
default=HorizontalAlignments.LEFT,
|
||||||
)
|
)
|
||||||
|
heading_1_text_decoration = MultipleFlagField(
|
||||||
|
default=[False, False, False, False],
|
||||||
|
num_flags=4,
|
||||||
|
db_default="0000",
|
||||||
|
help_text=("The text decoration flags [underline, strike, uppercase, italic]"),
|
||||||
|
)
|
||||||
heading_2_font_family = models.CharField(
|
heading_2_font_family = models.CharField(
|
||||||
max_length=250,
|
max_length=250,
|
||||||
default="inter",
|
default="inter",
|
||||||
|
@ -103,6 +109,12 @@ class TypographyThemeConfigBlock(ThemeConfigBlock):
|
||||||
max_length=10,
|
max_length=10,
|
||||||
default=HorizontalAlignments.LEFT,
|
default=HorizontalAlignments.LEFT,
|
||||||
)
|
)
|
||||||
|
heading_2_text_decoration = MultipleFlagField(
|
||||||
|
default=[False, False, False, False],
|
||||||
|
num_flags=4,
|
||||||
|
db_default="0000",
|
||||||
|
help_text=("The text decoration flags [underline, strike, uppercase, italic]"),
|
||||||
|
)
|
||||||
heading_3_font_family = models.CharField(
|
heading_3_font_family = models.CharField(
|
||||||
max_length=250,
|
max_length=250,
|
||||||
default="inter",
|
default="inter",
|
||||||
|
@ -122,6 +134,12 @@ class TypographyThemeConfigBlock(ThemeConfigBlock):
|
||||||
max_length=10,
|
max_length=10,
|
||||||
default=HorizontalAlignments.LEFT,
|
default=HorizontalAlignments.LEFT,
|
||||||
)
|
)
|
||||||
|
heading_3_text_decoration = MultipleFlagField(
|
||||||
|
default=[False, False, False, False],
|
||||||
|
num_flags=4,
|
||||||
|
db_default="0000",
|
||||||
|
help_text=("The text decoration flags [underline, strike, uppercase, italic]"),
|
||||||
|
)
|
||||||
heading_4_font_family = models.CharField(
|
heading_4_font_family = models.CharField(
|
||||||
max_length=250,
|
max_length=250,
|
||||||
default="inter",
|
default="inter",
|
||||||
|
@ -141,6 +159,12 @@ class TypographyThemeConfigBlock(ThemeConfigBlock):
|
||||||
max_length=10,
|
max_length=10,
|
||||||
default=HorizontalAlignments.LEFT,
|
default=HorizontalAlignments.LEFT,
|
||||||
)
|
)
|
||||||
|
heading_4_text_decoration = MultipleFlagField(
|
||||||
|
default=[False, False, False, False],
|
||||||
|
num_flags=4,
|
||||||
|
db_default="0000",
|
||||||
|
help_text=("The text decoration flags [underline, strike, uppercase, italic]"),
|
||||||
|
)
|
||||||
heading_5_font_family = models.CharField(
|
heading_5_font_family = models.CharField(
|
||||||
max_length=250,
|
max_length=250,
|
||||||
default="inter",
|
default="inter",
|
||||||
|
@ -160,6 +184,12 @@ class TypographyThemeConfigBlock(ThemeConfigBlock):
|
||||||
max_length=10,
|
max_length=10,
|
||||||
default=HorizontalAlignments.LEFT,
|
default=HorizontalAlignments.LEFT,
|
||||||
)
|
)
|
||||||
|
heading_5_text_decoration = MultipleFlagField(
|
||||||
|
default=[False, False, False, False],
|
||||||
|
num_flags=4,
|
||||||
|
db_default="0000",
|
||||||
|
help_text=("The text decoration flags [underline, strike, uppercase, italic]"),
|
||||||
|
)
|
||||||
heading_6_font_family = models.CharField(
|
heading_6_font_family = models.CharField(
|
||||||
max_length=250,
|
max_length=250,
|
||||||
default="inter",
|
default="inter",
|
||||||
|
@ -179,6 +209,12 @@ class TypographyThemeConfigBlock(ThemeConfigBlock):
|
||||||
max_length=10,
|
max_length=10,
|
||||||
default=HorizontalAlignments.LEFT,
|
default=HorizontalAlignments.LEFT,
|
||||||
)
|
)
|
||||||
|
heading_6_text_decoration = MultipleFlagField(
|
||||||
|
default=[False, False, False, False],
|
||||||
|
num_flags=4,
|
||||||
|
db_default="0000",
|
||||||
|
help_text=("The text decoration flags [underline, strike, uppercase, italic]"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ButtonThemeConfigBlockMixin(models.Model):
|
class ButtonThemeConfigBlockMixin(models.Model):
|
||||||
|
@ -318,6 +354,24 @@ class LinkThemeConfigBlockMixin(models.Model):
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="The hover color of links when active",
|
help_text="The hover color of links when active",
|
||||||
)
|
)
|
||||||
|
link_default_text_decoration = MultipleFlagField(
|
||||||
|
default=[True, False, False, False],
|
||||||
|
num_flags=4,
|
||||||
|
db_default="1000",
|
||||||
|
help_text=("The text decoration flags [underline, strike, uppercase, italic]"),
|
||||||
|
)
|
||||||
|
link_hover_text_decoration = MultipleFlagField(
|
||||||
|
default=[True, False, False, False],
|
||||||
|
num_flags=4,
|
||||||
|
db_default="1000",
|
||||||
|
help_text=("The text decoration flags [underline, strike, uppercase, italic]"),
|
||||||
|
)
|
||||||
|
link_active_text_decoration = MultipleFlagField(
|
||||||
|
default=[True, False, False, False],
|
||||||
|
num_flags=4,
|
||||||
|
db_default="1000",
|
||||||
|
help_text=("The text decoration flags [underline, strike, uppercase, italic]"),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
@ -573,9 +627,3 @@ class TableThemeConfigBlock(ThemeConfigBlock):
|
||||||
table_horizontal_separator_size = models.SmallIntegerField(
|
table_horizontal_separator_size = models.SmallIntegerField(
|
||||||
default=1, help_text="Table horizontal separator size"
|
default=1, help_text="Table horizontal separator size"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MenuThemeConfigBlock(
|
|
||||||
LinkThemeConfigBlockMixin, ButtonThemeConfigBlockMixin, ThemeConfigBlock
|
|
||||||
):
|
|
||||||
pass
|
|
||||||
|
|
|
@ -15,7 +15,6 @@ from .models import (
|
||||||
ImageThemeConfigBlock,
|
ImageThemeConfigBlock,
|
||||||
InputThemeConfigBlock,
|
InputThemeConfigBlock,
|
||||||
LinkThemeConfigBlock,
|
LinkThemeConfigBlock,
|
||||||
MenuThemeConfigBlock,
|
|
||||||
PageThemeConfigBlock,
|
PageThemeConfigBlock,
|
||||||
TableThemeConfigBlock,
|
TableThemeConfigBlock,
|
||||||
ThemeConfigBlock,
|
ThemeConfigBlock,
|
||||||
|
@ -33,6 +32,41 @@ class TypographyThemeConfigBlockType(ThemeConfigBlockType):
|
||||||
type = "typography"
|
type = "typography"
|
||||||
model_class = TypographyThemeConfigBlock
|
model_class = TypographyThemeConfigBlock
|
||||||
|
|
||||||
|
@property
|
||||||
|
def serializer_field_overrides(self):
|
||||||
|
return {
|
||||||
|
"heading_1_text_decoration": serializers.ListField(
|
||||||
|
child=serializers.BooleanField(),
|
||||||
|
help_text="Text decoration: [underline, stroke, uppercase, italic]",
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
"heading_2_text_decoration": serializers.ListField(
|
||||||
|
child=serializers.BooleanField(),
|
||||||
|
help_text="Text decoration: [underline, stroke, uppercase, italic]",
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
"heading_3_text_decoration": serializers.ListField(
|
||||||
|
child=serializers.BooleanField(),
|
||||||
|
help_text="Text decoration: [underline, stroke, uppercase, italic]",
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
"heading_4_text_decoration": serializers.ListField(
|
||||||
|
child=serializers.BooleanField(),
|
||||||
|
help_text="Text decoration: [underline, stroke, uppercase, italic]",
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
"heading_5_text_decoration": serializers.ListField(
|
||||||
|
child=serializers.BooleanField(),
|
||||||
|
help_text="Text decoration: [underline, stroke, uppercase, italic]",
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
"heading_6_text_decoration": serializers.ListField(
|
||||||
|
child=serializers.BooleanField(),
|
||||||
|
help_text="Text decoration: [underline, stroke, uppercase, italic]",
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
def import_serialized(
|
def import_serialized(
|
||||||
self,
|
self,
|
||||||
parent: Any,
|
parent: Any,
|
||||||
|
@ -64,6 +98,26 @@ class LinkThemeConfigBlockType(ThemeConfigBlockType):
|
||||||
type = "link"
|
type = "link"
|
||||||
model_class = LinkThemeConfigBlock
|
model_class = LinkThemeConfigBlock
|
||||||
|
|
||||||
|
@property
|
||||||
|
def serializer_field_overrides(self):
|
||||||
|
return {
|
||||||
|
"link_default_text_decoration": serializers.ListField(
|
||||||
|
child=serializers.BooleanField(),
|
||||||
|
help_text="Default text decoration: [underline, stroke, uppercase, italic]",
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
"link_hover_text_decoration": serializers.ListField(
|
||||||
|
child=serializers.BooleanField(),
|
||||||
|
help_text="Hover text decoration: [underline, stroke, uppercase, italic]",
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
"link_active_text_decoration": serializers.ListField(
|
||||||
|
child=serializers.BooleanField(),
|
||||||
|
help_text="Active text decoration: [underline, stroke, uppercase, italic]",
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ImageThemeConfigBlockType(ThemeConfigBlockType):
|
class ImageThemeConfigBlockType(ThemeConfigBlockType):
|
||||||
type = "image"
|
type = "image"
|
||||||
|
@ -183,8 +237,3 @@ class InputThemeConfigBlockType(ThemeConfigBlockType):
|
||||||
class TableThemeConfigBlockType(ThemeConfigBlockType):
|
class TableThemeConfigBlockType(ThemeConfigBlockType):
|
||||||
type = "table"
|
type = "table"
|
||||||
model_class = TableThemeConfigBlock
|
model_class = TableThemeConfigBlock
|
||||||
|
|
||||||
|
|
||||||
class MenuThemeConfigBlockType(ThemeConfigBlockType):
|
|
||||||
type = "menu"
|
|
||||||
model_class = MenuThemeConfigBlock
|
|
||||||
|
|
|
@ -257,3 +257,84 @@ class LenientDecimalField(models.Field):
|
||||||
**kwargs,
|
**kwargs,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def default_boolean_list(num_flags):
|
||||||
|
"""Returns a default list of False values"""
|
||||||
|
|
||||||
|
return [False] * num_flags
|
||||||
|
|
||||||
|
|
||||||
|
class MultipleFlagField(models.CharField):
|
||||||
|
"""Stores a list of booleans as a binary string"""
|
||||||
|
|
||||||
|
def __init__(self, num_flags=8, default=None, *args, **kwargs):
|
||||||
|
self.num_flags = num_flags
|
||||||
|
kwargs.setdefault("max_length", num_flags) # Ensures max length is set
|
||||||
|
|
||||||
|
# Handle list-based default values properly
|
||||||
|
if default is None:
|
||||||
|
default = default_boolean_list(num_flags)
|
||||||
|
if isinstance(default, list):
|
||||||
|
if len(default) != num_flags:
|
||||||
|
raise ValueError(f"Default list must have exactly {num_flags} elements")
|
||||||
|
# Convert list to string representation
|
||||||
|
kwargs["default"] = "".join("1" if flag else "0" for flag in default)
|
||||||
|
elif isinstance(default, str):
|
||||||
|
if len(default) != num_flags or not set(default).issubset({"0", "1"}):
|
||||||
|
raise ValueError(
|
||||||
|
f"Default string must be exactly {num_flags} characters of "
|
||||||
|
"'0' or '1'"
|
||||||
|
)
|
||||||
|
kwargs["default"] = default
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
"Default must be a list of booleans, a binary string, or None"
|
||||||
|
)
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def from_db_value(self, value, expression, connection):
|
||||||
|
"""
|
||||||
|
Converts the stored binary string into a list of booleans when retrieving
|
||||||
|
from the database
|
||||||
|
"""
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
return default_boolean_list(self.num_flags)
|
||||||
|
return [char == "1" for char in value]
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
"""Ensures the value is always returned as a list of booleans"""
|
||||||
|
|
||||||
|
if isinstance(value, list):
|
||||||
|
return value
|
||||||
|
return [char == "1" for char in value]
|
||||||
|
|
||||||
|
def get_prep_value(self, value):
|
||||||
|
"""Converts the list of booleans into a binary string for database storage"""
|
||||||
|
|
||||||
|
if isinstance(value, str):
|
||||||
|
# If Django passes the default value as a string, assume it's already in
|
||||||
|
# correct format
|
||||||
|
if len(value) != self.num_flags or not set(value).issubset({"0", "1"}):
|
||||||
|
raise ValueError(
|
||||||
|
f"Stored string must have exactly {self.num_flags} characters of "
|
||||||
|
"'0' or '1'"
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
elif isinstance(value, list):
|
||||||
|
if len(value) != self.num_flags:
|
||||||
|
raise ValueError(f"List must have exactly {self.num_flags} elements")
|
||||||
|
return "".join("1" if flag else "0" for flag in value)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
"Value must be a list of booleans or a valid binary string"
|
||||||
|
)
|
||||||
|
|
||||||
|
def deconstruct(self):
|
||||||
|
"""Ensures Django migrations correctly store and restore num_flags"""
|
||||||
|
|
||||||
|
name, path, args, kwargs = super().deconstruct()
|
||||||
|
kwargs["num_flags"] = self.num_flags # Add num_flags explicitly
|
||||||
|
return name, path, args, kwargs
|
||||||
|
|
|
@ -621,31 +621,37 @@ def test_builder_application_export(data_fixture):
|
||||||
"heading_1_font_weight": "bold",
|
"heading_1_font_weight": "bold",
|
||||||
"heading_1_text_color": "#070810ff",
|
"heading_1_text_color": "#070810ff",
|
||||||
"heading_1_text_alignment": "left",
|
"heading_1_text_alignment": "left",
|
||||||
|
"heading_1_text_decoration": [False, False, False, False],
|
||||||
"heading_2_font_family": "inter",
|
"heading_2_font_family": "inter",
|
||||||
"heading_2_font_size": 20,
|
"heading_2_font_size": 20,
|
||||||
"heading_2_font_weight": "semi-bold",
|
"heading_2_font_weight": "semi-bold",
|
||||||
"heading_2_text_color": "#070810ff",
|
"heading_2_text_color": "#070810ff",
|
||||||
"heading_2_text_alignment": "left",
|
"heading_2_text_alignment": "left",
|
||||||
|
"heading_2_text_decoration": [False, False, False, False],
|
||||||
"heading_3_font_family": "inter",
|
"heading_3_font_family": "inter",
|
||||||
"heading_3_font_size": 16,
|
"heading_3_font_size": 16,
|
||||||
"heading_3_font_weight": "medium",
|
"heading_3_font_weight": "medium",
|
||||||
"heading_3_text_color": "#070810ff",
|
"heading_3_text_color": "#070810ff",
|
||||||
"heading_3_text_alignment": "left",
|
"heading_3_text_alignment": "left",
|
||||||
|
"heading_3_text_decoration": [False, False, False, False],
|
||||||
"heading_4_font_family": "inter",
|
"heading_4_font_family": "inter",
|
||||||
"heading_4_font_size": 16,
|
"heading_4_font_size": 16,
|
||||||
"heading_4_font_weight": "medium",
|
"heading_4_font_weight": "medium",
|
||||||
"heading_4_text_color": "#070810ff",
|
"heading_4_text_color": "#070810ff",
|
||||||
"heading_4_text_alignment": "left",
|
"heading_4_text_alignment": "left",
|
||||||
|
"heading_4_text_decoration": [False, False, False, False],
|
||||||
"heading_5_font_family": "inter",
|
"heading_5_font_family": "inter",
|
||||||
"heading_5_font_size": 14,
|
"heading_5_font_size": 14,
|
||||||
"heading_5_font_weight": "regular",
|
"heading_5_font_weight": "regular",
|
||||||
"heading_5_text_color": "#070810ff",
|
"heading_5_text_color": "#070810ff",
|
||||||
"heading_5_text_alignment": "left",
|
"heading_5_text_alignment": "left",
|
||||||
|
"heading_5_text_decoration": [False, False, False, False],
|
||||||
"heading_6_font_family": "inter",
|
"heading_6_font_family": "inter",
|
||||||
"heading_6_font_size": 14,
|
"heading_6_font_size": 14,
|
||||||
"heading_6_font_weight": "regular",
|
"heading_6_font_weight": "regular",
|
||||||
"heading_6_text_color": "#202128",
|
"heading_6_text_color": "#202128",
|
||||||
"heading_6_text_alignment": "left",
|
"heading_6_text_alignment": "left",
|
||||||
|
"heading_6_text_decoration": [False, False, False, False],
|
||||||
"button_font_family": "inter",
|
"button_font_family": "inter",
|
||||||
"button_font_size": 13,
|
"button_font_size": 13,
|
||||||
"button_font_weight": "regular",
|
"button_font_weight": "regular",
|
||||||
|
@ -673,6 +679,9 @@ def test_builder_application_export(data_fixture):
|
||||||
"link_text_color": "primary",
|
"link_text_color": "primary",
|
||||||
"link_hover_text_color": "#96baf6ff",
|
"link_hover_text_color": "#96baf6ff",
|
||||||
"link_active_text_color": "#275d9f",
|
"link_active_text_color": "#275d9f",
|
||||||
|
"link_active_text_decoration": [True, False, False, False],
|
||||||
|
"link_default_text_decoration": [True, False, False, False],
|
||||||
|
"link_hover_text_decoration": [True, False, False, False],
|
||||||
"image_alignment": "left",
|
"image_alignment": "left",
|
||||||
"image_border_radius": 0,
|
"image_border_radius": 0,
|
||||||
"image_max_width": 100,
|
"image_max_width": 100,
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"type": "feature",
|
||||||
|
"message": "Added theme setting to configure Link decoration.",
|
||||||
|
"domain": "builder",
|
||||||
|
"issue_number": 3515,
|
||||||
|
"bullet_points": [],
|
||||||
|
"created_at": "2025-03-14"
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
<template>
|
||||||
|
<div class="text-decoration-selector">
|
||||||
|
<SwitchButton
|
||||||
|
:value="value[0]"
|
||||||
|
:icon="'iconoir-underline'"
|
||||||
|
:title="$t('textDecorationSelector.underline')"
|
||||||
|
@input="toggle(0)"
|
||||||
|
/>
|
||||||
|
<SwitchButton
|
||||||
|
:value="value[1]"
|
||||||
|
:icon="'iconoir-strikethrough'"
|
||||||
|
:title="$t('textDecorationSelector.stroke')"
|
||||||
|
@input="toggle(1)"
|
||||||
|
/>
|
||||||
|
<SwitchButton
|
||||||
|
:value="value[2]"
|
||||||
|
:icon="'iconoir-text'"
|
||||||
|
:title="$t('textDecorationSelector.uppercase')"
|
||||||
|
@input="toggle(2)"
|
||||||
|
/>
|
||||||
|
<SwitchButton
|
||||||
|
:value="value[3]"
|
||||||
|
:icon="'iconoir-italic'"
|
||||||
|
:title="$t('textDecorationSelector.italic')"
|
||||||
|
@input="toggle(3)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'TextDecorationSelector',
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => [false, false, false, false],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggle(index) {
|
||||||
|
this.$emit(
|
||||||
|
'input',
|
||||||
|
this.value.map((v, i) => (i === index ? !v : v))
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -99,6 +99,22 @@
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
horizontal-narrow
|
||||||
|
small-label
|
||||||
|
class="margin-bottom-2"
|
||||||
|
:label="$t('linkThemeConfigBlock.decoration')"
|
||||||
|
>
|
||||||
|
<TextDecorationSelector
|
||||||
|
v-model="values.link_default_text_decoration"
|
||||||
|
/>
|
||||||
|
<template #after-input>
|
||||||
|
<ResetButton
|
||||||
|
v-model="values.link_default_text_decoration"
|
||||||
|
:default-value="theme?.link_default_text_decoration"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</FormGroup>
|
||||||
</template>
|
</template>
|
||||||
<template #preview>
|
<template #preview>
|
||||||
<ABLink url="">{{ $t('linkThemeConfigBlock.link') }}</ABLink>
|
<ABLink url="">{{ $t('linkThemeConfigBlock.link') }}</ABLink>
|
||||||
|
@ -126,6 +142,20 @@
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
horizontal-narrow
|
||||||
|
small-label
|
||||||
|
class="margin-bottom-2"
|
||||||
|
:label="$t('linkThemeConfigBlock.decoration')"
|
||||||
|
>
|
||||||
|
<TextDecorationSelector v-model="values.link_hover_text_decoration" />
|
||||||
|
<template #after-input>
|
||||||
|
<ResetButton
|
||||||
|
v-model="values.link_hover_text_decoration"
|
||||||
|
:default-value="theme?.link_hover_text_decoration"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</FormGroup>
|
||||||
</template>
|
</template>
|
||||||
<template #preview>
|
<template #preview>
|
||||||
<ABLink url="" class="ab-link--force-hover">
|
<ABLink url="" class="ab-link--force-hover">
|
||||||
|
@ -155,6 +185,22 @@
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
horizontal-narrow
|
||||||
|
small-label
|
||||||
|
class="margin-bottom-2"
|
||||||
|
:label="$t('linkThemeConfigBlock.decoration')"
|
||||||
|
>
|
||||||
|
<TextDecorationSelector
|
||||||
|
v-model="values.link_active_text_decoration"
|
||||||
|
/>
|
||||||
|
<template #after-input>
|
||||||
|
<ResetButton
|
||||||
|
v-model="values.link_active_text_decoration"
|
||||||
|
:default-value="theme?.link_active_text_decoration"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</FormGroup>
|
||||||
</template>
|
</template>
|
||||||
<template #preview>
|
<template #preview>
|
||||||
<ABLink url="" class="ab-link--force-active">
|
<ABLink url="" class="ab-link--force-active">
|
||||||
|
@ -174,6 +220,7 @@ import HorizontalAlignmentsSelector from '@baserow/modules/builder/components/Ho
|
||||||
import FontFamilySelector from '@baserow/modules/builder/components/FontFamilySelector'
|
import FontFamilySelector from '@baserow/modules/builder/components/FontFamilySelector'
|
||||||
import FontWeightSelector from '@baserow/modules/builder/components/FontWeightSelector'
|
import FontWeightSelector from '@baserow/modules/builder/components/FontWeightSelector'
|
||||||
import PixelValueSelector from '@baserow/modules/builder/components/PixelValueSelector'
|
import PixelValueSelector from '@baserow/modules/builder/components/PixelValueSelector'
|
||||||
|
import TextDecorationSelector from '@baserow/modules/builder/components/TextDecorationSelector'
|
||||||
import {
|
import {
|
||||||
required,
|
required,
|
||||||
integer,
|
integer,
|
||||||
|
@ -199,6 +246,7 @@ export default {
|
||||||
FontFamilySelector,
|
FontFamilySelector,
|
||||||
FontWeightSelector,
|
FontWeightSelector,
|
||||||
PixelValueSelector,
|
PixelValueSelector,
|
||||||
|
TextDecorationSelector,
|
||||||
},
|
},
|
||||||
mixins: [themeConfigBlock],
|
mixins: [themeConfigBlock],
|
||||||
setup() {
|
setup() {
|
||||||
|
@ -226,6 +274,9 @@ export default {
|
||||||
link_font_family: this.theme?.link_font_family,
|
link_font_family: this.theme?.link_font_family,
|
||||||
link_font_weight: this.theme?.link_font_weight,
|
link_font_weight: this.theme?.link_font_weight,
|
||||||
link_font_size: this.theme?.link_font_size,
|
link_font_size: this.theme?.link_font_size,
|
||||||
|
link_default_text_decoration: this.theme?.link_default_text_decoration,
|
||||||
|
link_hover_text_decoration: this.theme?.link_hover_text_decoration,
|
||||||
|
link_active_text_decoration: this.theme?.link_active_text_decoration,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -267,6 +318,9 @@ export default {
|
||||||
link_active_text_color: {},
|
link_active_text_color: {},
|
||||||
link_font_family: {},
|
link_font_family: {},
|
||||||
link_font_weight: {},
|
link_font_weight: {},
|
||||||
|
link_default_text_decoration: {},
|
||||||
|
link_hover_text_decoration: {},
|
||||||
|
link_active_text_decoration: {},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -204,6 +204,20 @@
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
horizontal-narrow
|
||||||
|
small-label
|
||||||
|
class="margin-bottom-2"
|
||||||
|
:label="$t('typographyThemeConfigBlock.decoration')"
|
||||||
|
>
|
||||||
|
<TextDecorationSelector
|
||||||
|
v-model="values[`heading_${level}_text_decoration`]" />
|
||||||
|
<template #after-input>
|
||||||
|
<ResetButton
|
||||||
|
v-model="values[`heading_${level}_text_decoration`]"
|
||||||
|
:default-value="theme?.[`heading_${level}_text_decoration`]"
|
||||||
|
/> </template
|
||||||
|
></FormGroup>
|
||||||
</template>
|
</template>
|
||||||
<template #preview>
|
<template #preview>
|
||||||
<ABHeading
|
<ABHeading
|
||||||
|
@ -234,6 +248,7 @@ import HorizontalAlignmentsSelector from '@baserow/modules/builder/components/Ho
|
||||||
import FontFamilySelector from '@baserow/modules/builder/components/FontFamilySelector'
|
import FontFamilySelector from '@baserow/modules/builder/components/FontFamilySelector'
|
||||||
import FontWeightSelector from '@baserow/modules/builder/components/FontWeightSelector'
|
import FontWeightSelector from '@baserow/modules/builder/components/FontWeightSelector'
|
||||||
import PixelValueSelector from '@baserow/modules/builder/components/PixelValueSelector'
|
import PixelValueSelector from '@baserow/modules/builder/components/PixelValueSelector'
|
||||||
|
import TextDecorationSelector from '@baserow/modules/builder/components/TextDecorationSelector'
|
||||||
import { DEFAULT_FONT_SIZE_PX } from '@baserow/modules/builder/defaultStyles'
|
import { DEFAULT_FONT_SIZE_PX } from '@baserow/modules/builder/defaultStyles'
|
||||||
|
|
||||||
const fontSizeMin = 1
|
const fontSizeMin = 1
|
||||||
|
@ -250,6 +265,7 @@ export default {
|
||||||
FontFamilySelector,
|
FontFamilySelector,
|
||||||
FontWeightSelector,
|
FontWeightSelector,
|
||||||
PixelValueSelector,
|
PixelValueSelector,
|
||||||
|
TextDecorationSelector,
|
||||||
},
|
},
|
||||||
mixins: [themeConfigBlock],
|
mixins: [themeConfigBlock],
|
||||||
setup() {
|
setup() {
|
||||||
|
@ -269,6 +285,7 @@ export default {
|
||||||
o[`heading_${i}_font_family`] = ''
|
o[`heading_${i}_font_family`] = ''
|
||||||
o[`heading_${i}_font_weight`] = ''
|
o[`heading_${i}_font_weight`] = ''
|
||||||
o[`heading_${i}_text_alignment`] = ''
|
o[`heading_${i}_text_alignment`] = ''
|
||||||
|
o[`heading_${i}_text_decoration`] = [false, false, false]
|
||||||
return o
|
return o
|
||||||
}, {}),
|
}, {}),
|
||||||
},
|
},
|
||||||
|
|
|
@ -140,11 +140,11 @@
|
||||||
"elementInProgress": "Adding element..."
|
"elementInProgress": "Adding element..."
|
||||||
},
|
},
|
||||||
"addElementCategory": {
|
"addElementCategory": {
|
||||||
"suggestedElement": "Suggested elements",
|
"suggestedElement": "Suggested elements",
|
||||||
"baseElement": "Base elements",
|
"baseElement": "Base elements",
|
||||||
"layoutElement": "Layout elements",
|
"layoutElement": "Layout elements",
|
||||||
"formElement": "Form elements"
|
"formElement": "Form elements"
|
||||||
},
|
},
|
||||||
"elementMenu": {
|
"elementMenu": {
|
||||||
"moveUp": "Move up",
|
"moveUp": "Move up",
|
||||||
"moveDown": "Move down",
|
"moveDown": "Move down",
|
||||||
|
@ -563,6 +563,7 @@
|
||||||
"weight": "Weight",
|
"weight": "Weight",
|
||||||
"textAlignment": "Alignment",
|
"textAlignment": "Alignment",
|
||||||
"bodyLabel": "Body",
|
"bodyLabel": "Body",
|
||||||
|
"decoration": "Text decoration",
|
||||||
"fontFamily": "Font"
|
"fontFamily": "Font"
|
||||||
},
|
},
|
||||||
"fontWeightType": {
|
"fontWeightType": {
|
||||||
|
@ -595,6 +596,10 @@
|
||||||
"size": "Font size",
|
"size": "Font size",
|
||||||
"weight": "Font weight"
|
"weight": "Font weight"
|
||||||
},
|
},
|
||||||
|
"linkDecorations": {
|
||||||
|
"normal": "Normal",
|
||||||
|
"plain": "Plain"
|
||||||
|
},
|
||||||
"linkThemeConfigBlock": {
|
"linkThemeConfigBlock": {
|
||||||
"color": "Color",
|
"color": "Color",
|
||||||
"link": "Link",
|
"link": "Link",
|
||||||
|
@ -604,7 +609,8 @@
|
||||||
"alignment": "Alignment",
|
"alignment": "Alignment",
|
||||||
"fontFamily": "Font",
|
"fontFamily": "Font",
|
||||||
"size": "Font size",
|
"size": "Font size",
|
||||||
"weight": "Font weight"
|
"weight": "Font weight",
|
||||||
|
"decoration": "Text decoration"
|
||||||
},
|
},
|
||||||
"inputThemeConfigBlock": {
|
"inputThemeConfigBlock": {
|
||||||
"label": "Label",
|
"label": "Label",
|
||||||
|
@ -972,5 +978,11 @@
|
||||||
"authProviderWithModal": {
|
"authProviderWithModal": {
|
||||||
"authProviderInError": "Please edit this provider to fix the error.",
|
"authProviderInError": "Please edit this provider to fix the error.",
|
||||||
"title": "Edit provider: {name}"
|
"title": "Edit provider: {name}"
|
||||||
|
},
|
||||||
|
"textDecorationSelector": {
|
||||||
|
"underline": "Underline",
|
||||||
|
"stroke": "Stroke",
|
||||||
|
"italic": "Italic",
|
||||||
|
"uppercase": "Uppercase"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -289,6 +289,36 @@ export class TypographyThemeConfigBlockType extends ThemeConfigBlockType {
|
||||||
`heading_${level}_font_weight`,
|
`heading_${level}_font_weight`,
|
||||||
`--heading-h${level}-font-weight`
|
`--heading-h${level}-font-weight`
|
||||||
)
|
)
|
||||||
|
style.addIfExists(
|
||||||
|
theme,
|
||||||
|
`heading_${level}_text_decoration`,
|
||||||
|
`--heading-h${level}-text-decoration`,
|
||||||
|
(v) => {
|
||||||
|
const value = []
|
||||||
|
if (v[0]) {
|
||||||
|
value.push('underline')
|
||||||
|
}
|
||||||
|
if (v[1]) {
|
||||||
|
value.push('line-through')
|
||||||
|
}
|
||||||
|
if (value.length === 0) {
|
||||||
|
return 'none'
|
||||||
|
}
|
||||||
|
return value.join(' ')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
style.addIfExists(
|
||||||
|
theme,
|
||||||
|
`heading_${level}_text_decoration`,
|
||||||
|
`--heading-h${level}-text-transform`,
|
||||||
|
(v) => (v[2] ? 'uppercase' : 'none')
|
||||||
|
)
|
||||||
|
style.addIfExists(
|
||||||
|
theme,
|
||||||
|
`heading_${level}_text_decoration`,
|
||||||
|
`--heading-h${level}-font-style`,
|
||||||
|
(v) => (v[3] ? 'italic' : 'none')
|
||||||
|
)
|
||||||
})
|
})
|
||||||
style.addPixelValueIfExists(theme, `body_font_size`)
|
style.addPixelValueIfExists(theme, `body_font_size`)
|
||||||
style.addColorIfExists(theme, `body_text_color`)
|
style.addColorIfExists(theme, `body_text_color`)
|
||||||
|
@ -398,6 +428,96 @@ export class LinkThemeConfigBlockType extends ThemeConfigBlockType {
|
||||||
const fontFamilyType = this.app.$registry.get('fontFamily', v)
|
const fontFamilyType = this.app.$registry.get('fontFamily', v)
|
||||||
return `"${fontFamilyType.name}","${fontFamilyType.safeFont}"`
|
return `"${fontFamilyType.name}","${fontFamilyType.safeFont}"`
|
||||||
})
|
})
|
||||||
|
style.addIfExists(
|
||||||
|
theme,
|
||||||
|
'link_default_text_decoration',
|
||||||
|
'--link-default-text-decoration',
|
||||||
|
(v) => {
|
||||||
|
const value = []
|
||||||
|
if (v[0]) {
|
||||||
|
value.push('underline')
|
||||||
|
}
|
||||||
|
if (v[1]) {
|
||||||
|
value.push('line-through')
|
||||||
|
}
|
||||||
|
if (value.length === 0) {
|
||||||
|
return 'none'
|
||||||
|
}
|
||||||
|
return value.join(' ')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
style.addIfExists(
|
||||||
|
theme,
|
||||||
|
'link_default_text_decoration',
|
||||||
|
'--link-default-text-transform',
|
||||||
|
(v) => (v[2] ? 'uppercase' : 'none')
|
||||||
|
)
|
||||||
|
style.addIfExists(
|
||||||
|
theme,
|
||||||
|
'link_default_text_decoration',
|
||||||
|
'--link-default-font-style',
|
||||||
|
(v) => (v[3] ? 'italic' : 'none')
|
||||||
|
)
|
||||||
|
style.addIfExists(
|
||||||
|
theme,
|
||||||
|
'link_hover_text_decoration',
|
||||||
|
'--link-hover-text-decoration',
|
||||||
|
(v) => {
|
||||||
|
const value = []
|
||||||
|
if (v[0]) {
|
||||||
|
value.push('underline')
|
||||||
|
}
|
||||||
|
if (v[1]) {
|
||||||
|
value.push('line-through')
|
||||||
|
}
|
||||||
|
if (value.length === 0) {
|
||||||
|
return 'none'
|
||||||
|
}
|
||||||
|
return value.join(' ')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
style.addIfExists(
|
||||||
|
theme,
|
||||||
|
'link_hover_text_decoration',
|
||||||
|
'--link-hover-text-transform',
|
||||||
|
(v) => (v[2] ? 'uppercase' : 'none')
|
||||||
|
)
|
||||||
|
style.addIfExists(
|
||||||
|
theme,
|
||||||
|
'link_hover_text_decoration',
|
||||||
|
'--link-hover-font-style',
|
||||||
|
(v) => (v[3] ? 'italic' : 'none')
|
||||||
|
)
|
||||||
|
style.addIfExists(
|
||||||
|
theme,
|
||||||
|
'link_active_text_decoration',
|
||||||
|
'--link-active-text-decoration',
|
||||||
|
(v) => {
|
||||||
|
const value = []
|
||||||
|
if (v[0]) {
|
||||||
|
value.push('underline')
|
||||||
|
}
|
||||||
|
if (v[1]) {
|
||||||
|
value.push('line-through')
|
||||||
|
}
|
||||||
|
if (value.length === 0) {
|
||||||
|
return 'none'
|
||||||
|
}
|
||||||
|
return value.join(' ')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
style.addIfExists(
|
||||||
|
theme,
|
||||||
|
'link_active_text_decoration',
|
||||||
|
'--link-active-text-transform',
|
||||||
|
(v) => (v[2] ? 'uppercase' : 'none')
|
||||||
|
)
|
||||||
|
style.addIfExists(
|
||||||
|
theme,
|
||||||
|
'link_active_text_decoration',
|
||||||
|
'--link-active-font-style',
|
||||||
|
(v) => (v[3] ? 'italic' : 'none')
|
||||||
|
)
|
||||||
style.addPixelValueIfExists(theme, `link_font_size`)
|
style.addPixelValueIfExists(theme, `link_font_size`)
|
||||||
style.addFontWeightIfExists(theme, `link_font_weight`)
|
style.addFontWeightIfExists(theme, `link_font_weight`)
|
||||||
return style.toObject()
|
return style.toObject()
|
||||||
|
|
|
@ -41,3 +41,4 @@
|
||||||
@import 'auth_provider_with_modal';
|
@import 'auth_provider_with_modal';
|
||||||
@import 'side_bar';
|
@import 'side_bar';
|
||||||
@import 'custom_color_input';
|
@import 'custom_color_input';
|
||||||
|
@import 'text_decoration_selector';
|
||||||
|
|
|
@ -8,6 +8,9 @@
|
||||||
font-weight: var(--heading-h1-font-weight, 700);
|
font-weight: var(--heading-h1-font-weight, 700);
|
||||||
text-align: var(--heading-h1-text-alignment, left);
|
text-align: var(--heading-h1-text-alignment, left);
|
||||||
font-family: var(--heading-h1-font-family, Inter);
|
font-family: var(--heading-h1-font-family, Inter);
|
||||||
|
text-decoration: var(--heading-h1-text-decoration, none);
|
||||||
|
text-transform: var(--heading-h1-text-transform, none);
|
||||||
|
font-style: var(--heading-h1-font-style, none);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ab-heading--h2 {
|
.ab-heading--h2 {
|
||||||
|
@ -16,6 +19,9 @@
|
||||||
font-weight: var(--heading-h2-font-weight, 600);
|
font-weight: var(--heading-h2-font-weight, 600);
|
||||||
text-align: var(--heading-h2-text-alignment, left);
|
text-align: var(--heading-h2-text-alignment, left);
|
||||||
font-family: var(--heading-h2-font-family, Inter);
|
font-family: var(--heading-h2-font-family, Inter);
|
||||||
|
text-decoration: var(--heading-h2-text-decoration, none);
|
||||||
|
text-transform: var(--heading-h2-text-transform, none);
|
||||||
|
font-style: var(--heading-h2-font-style, none);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ab-heading--h3 {
|
.ab-heading--h3 {
|
||||||
|
@ -24,6 +30,9 @@
|
||||||
font-weight: var(--heading-h3-font-weight, 500);
|
font-weight: var(--heading-h3-font-weight, 500);
|
||||||
text-align: var(--heading-h3-text-alignment, left);
|
text-align: var(--heading-h3-text-alignment, left);
|
||||||
font-family: var(--heading-h3-font-family, Inter);
|
font-family: var(--heading-h3-font-family, Inter);
|
||||||
|
text-decoration: var(--heading-h3-text-decoration, none);
|
||||||
|
text-transform: var(--heading-h3-text-transform, none);
|
||||||
|
font-style: var(--heading-h3-font-style, none);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ab-heading--h4 {
|
.ab-heading--h4 {
|
||||||
|
@ -32,6 +41,9 @@
|
||||||
font-weight: var(--heading-h4-font-weight, 500);
|
font-weight: var(--heading-h4-font-weight, 500);
|
||||||
text-align: var(--heading-h4-text-alignment, left);
|
text-align: var(--heading-h4-text-alignment, left);
|
||||||
font-family: var(--heading-h4-font-family, Inter);
|
font-family: var(--heading-h4-font-family, Inter);
|
||||||
|
text-decoration: var(--heading-h4-text-decoration, none);
|
||||||
|
text-transform: var(--heading-h4-text-transform, none);
|
||||||
|
font-style: var(--heading-h4-font-style, none);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ab-heading--h5 {
|
.ab-heading--h5 {
|
||||||
|
@ -40,6 +52,9 @@
|
||||||
font-weight: var(--heading-h5-font-weight, 400);
|
font-weight: var(--heading-h5-font-weight, 400);
|
||||||
text-align: var(--heading-h5-text-alignment, left);
|
text-align: var(--heading-h5-text-alignment, left);
|
||||||
font-family: var(--heading-h5-font-family, Inter);
|
font-family: var(--heading-h5-font-family, Inter);
|
||||||
|
text-decoration: var(--heading-h5-text-decoration, none);
|
||||||
|
text-transform: var(--heading-h5-text-transform, none);
|
||||||
|
font-style: var(--heading-h5-font-style, none);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ab-heading--h6 {
|
.ab-heading--h6 {
|
||||||
|
@ -48,5 +63,7 @@
|
||||||
font-weight: var(--heading-h6-font-weight, 400);
|
font-weight: var(--heading-h6-font-weight, 400);
|
||||||
text-align: var(--heading-h6-text-alignment, left);
|
text-align: var(--heading-h6-text-alignment, left);
|
||||||
font-family: var(--heading-h6-font-family, Inter);
|
font-family: var(--heading-h6-font-family, Inter);
|
||||||
font-style: italic;
|
text-decoration: var(--heading-h6-text-decoration, underline);
|
||||||
|
text-transform: var(--heading-h6-text-transform, none);
|
||||||
|
font-style: var(--heading-h6-font-style, italic);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
.ab-link {
|
.ab-link {
|
||||||
font-size: var(--link-font-size, 14px);
|
font-size: var(--link-font-size, 14px);
|
||||||
font-weight: var(--link-font-weight, 400);
|
font-weight: var(--link-font-weight, 400);
|
||||||
text-decoration: var(--link-text-decoration, underline);
|
text-decoration: var(--link-default-text-decoration, underline);
|
||||||
|
text-transform: var(--link-default-text-transform, none);
|
||||||
|
font-style: var(--link-default-font-style, none);
|
||||||
color: var(--link-text-color, $black);
|
color: var(--link-text-color, $black);
|
||||||
align-self: var(--force-self-alignment, var(--link-text-alignment, initial));
|
align-self: var(--force-self-alignment, var(--link-text-alignment, initial));
|
||||||
font-family: var(--link-font-family, Inter);
|
font-family: var(--link-font-family, Inter);
|
||||||
|
@ -10,10 +12,14 @@
|
||||||
&--force-hover {
|
&--force-hover {
|
||||||
color: var(--link-hover-text-color, $black);
|
color: var(--link-hover-text-color, $black);
|
||||||
text-decoration: var(--link-hover-text-decoration, underline);
|
text-decoration: var(--link-hover-text-decoration, underline);
|
||||||
|
text-transform: var(--link-hover-text-transform, none);
|
||||||
|
font-style: var(--link-hover-font-style, none);
|
||||||
}
|
}
|
||||||
|
|
||||||
&--force-active {
|
&--force-active {
|
||||||
color: var(--link-active-text-color, $black);
|
color: var(--link-active-text-color, $black);
|
||||||
text-decoration: var(--link-active-text-decoration, underline);
|
text-decoration: var(--link-active-text-decoration, underline);
|
||||||
|
text-transform: var(--link-active-text-transform, none);
|
||||||
|
font-style: var(--link-active-font-style, none);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,14 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Disable pointer events when in Page Editor.
|
||||||
|
*/
|
||||||
|
.element--read-only .ab-link {
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
.link-element__link {
|
.link-element__link {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: $black;
|
color: $black;
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
.text-decoration-selector {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
|
@ -21,6 +21,8 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding-right: 14px;
|
padding-right: 14px;
|
||||||
padding-top: 4px;
|
padding-top: 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
.theme-config-block--no-preview & {
|
.theme-config-block--no-preview & {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
65
web-frontend/modules/core/components/SwitchButton.vue
Normal file
65
web-frontend/modules/core/components/SwitchButton.vue
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
<template>
|
||||||
|
<ButtonIcon
|
||||||
|
type="secondary"
|
||||||
|
v-bind="restProps"
|
||||||
|
:loading="loading"
|
||||||
|
:disabled="disabled"
|
||||||
|
:icon="icon"
|
||||||
|
:title="title"
|
||||||
|
:active="value"
|
||||||
|
@click.prevent="select()"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</ButtonIcon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'SwitchButton',
|
||||||
|
model: {
|
||||||
|
prop: 'modelValue',
|
||||||
|
event: 'input',
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
restProps() {
|
||||||
|
const { value, modelValue, ...rest } = this.$attrs
|
||||||
|
return rest
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
select() {
|
||||||
|
if (this.disabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$emit('input', !this.value)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -61,6 +61,7 @@ import RadioButton from '@baserow/modules/core/components/RadioButton'
|
||||||
import Thumbnail from '@baserow/modules/core/components/Thumbnail'
|
import Thumbnail from '@baserow/modules/core/components/Thumbnail'
|
||||||
import ColorInput from '@baserow/modules/core/components/ColorInput'
|
import ColorInput from '@baserow/modules/core/components/ColorInput'
|
||||||
import SelectSearch from '@baserow/modules/core/components/SelectSearch'
|
import SelectSearch from '@baserow/modules/core/components/SelectSearch'
|
||||||
|
import SwitchButton from '@baserow/modules/core/components/SwitchButton'
|
||||||
|
|
||||||
function setupVue(Vue) {
|
function setupVue(Vue) {
|
||||||
Vue.component('Context', Context)
|
Vue.component('Context', Context)
|
||||||
|
@ -111,6 +112,7 @@ function setupVue(Vue) {
|
||||||
Vue.component('ReadOnlyForm', ReadOnlyForm)
|
Vue.component('ReadOnlyForm', ReadOnlyForm)
|
||||||
Vue.component('FormSection', FormSection)
|
Vue.component('FormSection', FormSection)
|
||||||
Vue.component('SegmentControl', SegmentControl)
|
Vue.component('SegmentControl', SegmentControl)
|
||||||
|
Vue.component('SwitchButton', SwitchButton)
|
||||||
|
|
||||||
Vue.filter('lowercase', lowercase)
|
Vue.filter('lowercase', lowercase)
|
||||||
Vue.filter('uppercase', uppercase)
|
Vue.filter('uppercase', uppercase)
|
||||||
|
|
Loading…
Add table
Reference in a new issue