diff --git a/backend/src/baserow/contrib/builder/api/domains/serializers.py b/backend/src/baserow/contrib/builder/api/domains/serializers.py index a17094a99..f1d92414c 100644 --- a/backend/src/baserow/contrib/builder/api/domains/serializers.py +++ b/backend/src/baserow/contrib/builder/api/domains/serializers.py @@ -49,7 +49,7 @@ class PublicElementSerializer(serializers.ModelSerializer): class Meta: model = Element - fields = ("id", "type") + fields = ("id", "type", "style_padding_top", "style_padding_bottom") extra_kwargs = { "id": {"read_only": True}, "type": {"read_only": True}, diff --git a/backend/src/baserow/contrib/builder/api/elements/serializers.py b/backend/src/baserow/contrib/builder/api/elements/serializers.py index 84591a3c1..c4e2d5178 100644 --- a/backend/src/baserow/contrib/builder/api/elements/serializers.py +++ b/backend/src/baserow/contrib/builder/api/elements/serializers.py @@ -22,7 +22,14 @@ class ElementSerializer(serializers.ModelSerializer): class Meta: model = Element - fields = ("id", "page_id", "type", "order") + fields = ( + "id", + "page_id", + "type", + "order", + "style_padding_top", + "style_padding_bottom", + ) extra_kwargs = { "id": {"read_only": True}, "page_id": {"read_only": True}, @@ -50,13 +57,13 @@ class CreateElementSerializer(serializers.ModelSerializer): class Meta: model = Element - fields = ("before_id", "type") + fields = ("before_id", "type", "style_padding_top", "style_padding_bottom") class UpdateElementSerializer(serializers.ModelSerializer): class Meta: model = Element - fields = [] + fields = ("style_padding_top", "style_padding_bottom") class MoveElementSerializer(serializers.Serializer): diff --git a/backend/src/baserow/contrib/builder/elements/element_types.py b/backend/src/baserow/contrib/builder/elements/element_types.py index af4317800..86a5751df 100644 --- a/backend/src/baserow/contrib/builder/elements/element_types.py +++ b/backend/src/baserow/contrib/builder/elements/element_types.py @@ -1,4 +1,3 @@ -from abc import ABC from typing import Dict, Optional from rest_framework import serializers @@ -21,17 +20,70 @@ from baserow.contrib.builder.types import ElementDict from baserow.core.user_files.models import UserFile -class BaseTextElementType(ElementType, ABC): +class HeadingElementType(ElementType): """ - Base class for text elements. + A simple heading element that can be used to display a title. """ + type = "heading" + model_class = HeadingElement + serializer_field_names = ["value", "level"] + allowed_fields = ["value", "level"] + + class SerializedDict(ElementDict): + value: Expression + level: int + + @property + def serializer_field_overrides(self): + from baserow.core.expression.serializers import ExpressionSerializer + + overrides = { + "value": ExpressionSerializer( + help_text="The value of the element. Must be an expression.", + required=False, + allow_blank=True, + default="", + ), + "level": serializers.IntegerField( + help_text="The level of the heading from 1 to 6.", + min_value=1, + max_value=6, + default=1, + ), + } + + return overrides + + def get_sample_params(self): + return { + "value": "Corporis perspiciatis", + "level": 2, + } + + +class ParagraphElementType(ElementType): + """ + A simple paragraph element that can be used to display a paragraph of text. + """ + + type = "paragraph" + model_class = ParagraphElement serializer_field_names = ["value"] allowed_fields = ["value"] class SerializedDict(ElementDict): value: Expression + def get_sample_params(self): + return { + "value": "Suscipit maxime eos ea vel commodi dolore. " + "Eum dicta sit rerum animi. Sint sapiente eum cupiditate nobis vel. " + "Maxime qui nam consequatur. " + "Asperiores corporis perspiciatis nam harum veritatis. " + "Impedit qui maxime aut illo quod ea molestias." + } + @property def serializer_field_overrides(self): from baserow.core.expression.serializers import ExpressionSerializer @@ -46,65 +98,7 @@ class BaseTextElementType(ElementType, ABC): } -class HeadingElementType(BaseTextElementType): - """ - A simple heading element that can be used to display a title. - """ - - type = "heading" - model_class = HeadingElement - - class SerializedDict(ElementDict): - value: Expression - level: int - - @property - def serializer_field_names(self): - return super().serializer_field_names + ["level"] - - @property - def allowed_fields(self): - return super().allowed_fields + ["level"] - - @property - def serializer_field_overrides(self): - overrides = { - "level": serializers.IntegerField( - help_text="The level of the heading from 1 to 6.", - min_value=1, - max_value=6, - default=1, - ) - } - overrides.update(super().serializer_field_overrides) - return overrides - - def get_sample_params(self): - return { - "value": "Corporis perspiciatis", - "level": 2, - } - - -class ParagraphElementType(BaseTextElementType): - """ - A simple paragraph element that can be used to display a paragraph of text. - """ - - type = "paragraph" - model_class = ParagraphElement - - def get_sample_params(self): - return { - "value": "Suscipit maxime eos ea vel commodi dolore. " - "Eum dicta sit rerum animi. Sint sapiente eum cupiditate nobis vel. " - "Maxime qui nam consequatur. " - "Asperiores corporis perspiciatis nam harum veritatis. " - "Impedit qui maxime aut illo quod ea molestias." - } - - -class LinkElementType(BaseTextElementType): +class LinkElementType(ElementType): """ A simple paragraph element that can be used to display a paragraph of text. """ @@ -112,38 +106,34 @@ class LinkElementType(BaseTextElementType): type = "link" model_class = LinkElement PATH_PARAM_TYPE_TO_PYTHON_TYPE_MAP = {"text": str, "numeric": int} + serializer_field_names = [ + "value", + "navigation_type", + "navigate_to_page_id", + "navigate_to_url", + "page_parameters", + "variant", + "target", + "width", + "alignment", + ] + allowed_fields = [ + "value", + "navigation_type", + "navigate_to_page_id", + "navigate_to_url", + "page_parameters", + "variant", + "target", + "width", + "alignment", + ] class SerializedDict(ElementDict): value: Expression destination: Expression open_new_tab: bool - @property - def serializer_field_names(self): - return super().serializer_field_names + [ - "navigation_type", - "navigate_to_page_id", - "navigate_to_url", - "page_parameters", - "variant", - "target", - "width", - "alignment", - ] - - @property - def allowed_fields(self): - return super().allowed_fields + [ - "navigation_type", - "navigate_to_page_id", - "navigate_to_url", - "page_parameters", - "variant", - "target", - "width", - "alignment", - ] - def import_serialized(self, page, serialized_values, id_mapping): serialized_copy = serialized_values.copy() if serialized_copy["navigate_to_page_id"]: @@ -160,6 +150,12 @@ class LinkElementType(BaseTextElementType): from baserow.core.expression.serializers import ExpressionSerializer overrides = { + "value": ExpressionSerializer( + help_text="The value of the element. Must be an expression.", + required=False, + allow_blank=True, + default="", + ), "navigation_type": serializers.ChoiceField( choices=LinkElement.NAVIGATION_TYPES.choices, help_text=LinkElement._meta.get_field("navigation_type").help_text, @@ -203,11 +199,11 @@ class LinkElementType(BaseTextElementType): required=False, ), } - overrides.update(super().serializer_field_overrides) return overrides def get_sample_params(self): return { + "value": "test", "navigation_type": "custom", "navigate_to_page_id": None, "navigate_to_url": "http://example.com", diff --git a/backend/src/baserow/contrib/builder/elements/handler.py b/backend/src/baserow/contrib/builder/elements/handler.py index a676575f8..f14fb1433 100644 --- a/backend/src/baserow/contrib/builder/elements/handler.py +++ b/backend/src/baserow/contrib/builder/elements/handler.py @@ -110,7 +110,7 @@ class ElementHandler: else: order = Element.get_last_order(page) - shared_allowed_fields = ["type"] + shared_allowed_fields = ["type", "style_padding_top", "style_padding_bottom"] allowed_values = extract_allowed( kwargs, shared_allowed_fields + element_type.allowed_fields ) @@ -145,7 +145,7 @@ class ElementHandler: element_type = element_type_registry.get_by_model(element) - shared_allowed_fields = [] + shared_allowed_fields = ["style_padding_top", "style_padding_bottom"] allowed_updates = extract_allowed( kwargs, shared_allowed_fields + element_type.allowed_fields ) diff --git a/backend/src/baserow/contrib/builder/elements/models.py b/backend/src/baserow/contrib/builder/elements/models.py index a976e615b..d9b84e354 100644 --- a/backend/src/baserow/contrib/builder/elements/models.py +++ b/backend/src/baserow/contrib/builder/elements/models.py @@ -51,6 +51,9 @@ class Element( on_delete=models.SET(get_default_element_content_type), ) + style_padding_top = models.PositiveIntegerField(default=10) + style_padding_bottom = models.PositiveIntegerField(default=10) + class Meta: ordering = ("order", "id") @@ -86,18 +89,7 @@ class Element( return cls.get_unique_orders_before_item(before, queryset)[0] -class BaseTextElement(Element): - """ - Base class for text elements. - """ - - value = ExpressionField(default="") - - class Meta: - abstract = True - - -class HeadingElement(BaseTextElement): +class HeadingElement(Element): """ A Heading element to display a title. """ @@ -109,18 +101,21 @@ class HeadingElement(BaseTextElement): H4 = 4 H5 = 5 + value = ExpressionField(default="") level = models.IntegerField( choices=HeadingLevel.choices, default=1, help_text="The level of the heading" ) -class ParagraphElement(BaseTextElement): +class ParagraphElement(Element): """ A simple paragraph. """ + value = ExpressionField(default="") -class LinkElement(BaseTextElement): + +class LinkElement(Element): """ A simple link. """ @@ -141,6 +136,7 @@ class LinkElement(BaseTextElement): AUTO = "auto" FULL = "full" + value = ExpressionField(default="") navigation_type = models.CharField( choices=NAVIGATION_TYPES.choices, help_text="The navigation type.", diff --git a/backend/src/baserow/contrib/builder/elements/registries.py b/backend/src/baserow/contrib/builder/elements/registries.py index 9dfa70dac..2ea6f0da9 100644 --- a/backend/src/baserow/contrib/builder/elements/registries.py +++ b/backend/src/baserow/contrib/builder/elements/registries.py @@ -58,7 +58,12 @@ class ElementType( other_properties = {key: getattr(element, key) for key in self.allowed_fields} serialized = self.SerializedDict( - id=element.id, type=self.type, order=element.order, **other_properties + id=element.id, + type=self.type, + order=element.order, + style_padding_top=element.style_padding_top, + style_padding_bottom=element.style_padding_bottom, + **other_properties ) return serialized diff --git a/backend/src/baserow/contrib/builder/migrations/0014_initial_styling.py b/backend/src/baserow/contrib/builder/migrations/0014_initial_styling.py new file mode 100644 index 000000000..b54c9fbab --- /dev/null +++ b/backend/src/baserow/contrib/builder/migrations/0014_initial_styling.py @@ -0,0 +1,22 @@ +# Generated by Django 3.2.18 on 2023-06-07 20:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("builder", "0013_datasource"), + ] + + operations = [ + migrations.AddField( + model_name="element", + name="style_padding_bottom", + field=models.PositiveIntegerField(default=10), + ), + migrations.AddField( + model_name="element", + name="style_padding_top", + field=models.PositiveIntegerField(default=10), + ), + ] diff --git a/backend/tests/baserow/contrib/builder/test_builder_application_type.py b/backend/tests/baserow/contrib/builder/test_builder_application_type.py index 875d6d4c1..9cfe64602 100644 --- a/backend/tests/baserow/contrib/builder/test_builder_application_type.py +++ b/backend/tests/baserow/contrib/builder/test_builder_application_type.py @@ -49,12 +49,16 @@ def test_builder_application_export(data_fixture): "id": element1.id, "type": "heading", "order": element1.order, + "style_padding_top": 10, + "style_padding_bottom": 10, "value": element1.value, "level": element1.level, }, { "id": element2.id, "type": "paragraph", + "style_padding_top": 10, + "style_padding_bottom": 10, "order": element2.order, "value": element2.value, }, @@ -70,6 +74,8 @@ def test_builder_application_export(data_fixture): { "id": element3.id, "type": "heading", + "style_padding_top": 10, + "style_padding_bottom": 10, "order": element3.order, "value": element3.value, "level": element3.level, diff --git a/web-frontend/modules/builder/components/elements/ElementMenu.vue b/web-frontend/modules/builder/components/elements/ElementMenu.vue index 8046cddde..e186bfe0b 100644 --- a/web-frontend/modules/builder/components/elements/ElementMenu.vue +++ b/web-frontend/modules/builder/components/elements/ElementMenu.vue @@ -1,38 +1,44 @@ <template> - <div class="element__menu"> + <div class="element-preview__menu"> <div v-if="isDuplicating" - class="loading element__menu-duplicate-loading" + class="loading element-preview__menu-duplicate-loading" ></div> - <a v-else class="element__menu-item" @click="$emit('duplicate')"> + <a v-else class="element-preview__menu-item" @click="$emit('duplicate')"> <i class="fas fa-copy"></i> - <span class="element__menu-item-description"> + <span class="element-preview__menu-item-description"> {{ $t('action.duplicate') }} </span> </a> <a - class="element__menu-item" + class="element-preview__menu-item" :class="{ disabled: moveUpDisabled }" @click="!moveUpDisabled && $emit('move', PLACEMENTS.BEFORE)" > <i class="fas fa-arrow-up"></i> - <span v-if="!moveUpDisabled" class="element__menu-item-description"> + <span + v-if="!moveUpDisabled" + class="element-preview__menu-item-description" + > {{ $t('elementMenu.moveUp') }} </span> </a> <a - class="element__menu-item" + class="element-preview__menu-item" :class="{ disabled: moveDownDisabled }" @click="!moveDownDisabled && $emit('move', PLACEMENTS.AFTER)" > <i class="fas fa-arrow-down"></i> - <span v-if="!moveDownDisabled" class="element__menu-item-description"> + <span + v-if="!moveDownDisabled" + class="element-preview__menu-item-description" + > {{ $t('elementMenu.moveDown') }} </span> </a> - <a class="element__menu-item" @click="$emit('delete')"> + <a class="element-preview__menu-item" @click="$emit('delete')"> <i class="fas fa-trash"></i> - <span class="element__menu-item-description"> + <span class="element-preview__menu-item-description"> {{ $t('action.delete') }} </span> </a> diff --git a/web-frontend/modules/builder/components/elements/ElementPreview.vue b/web-frontend/modules/builder/components/elements/ElementPreview.vue index 5cedf6240..04908c5ab 100644 --- a/web-frontend/modules/builder/components/elements/ElementPreview.vue +++ b/web-frontend/modules/builder/components/elements/ElementPreview.vue @@ -1,12 +1,15 @@ <template> <div - class="element" - :class="{ 'element--active': active, 'element--in-error': inError }" + class="element-preview" + :class="{ + 'element-preview--active': active, + 'element-preview--in-error': inError, + }" @click="$emit('selected')" > <InsertElementButton v-if="active" - class="element__insert--top" + class="element-preview__insert--top" @click="$emit('insert', PLACEMENTS.BEFORE)" /> <ElementMenu @@ -18,15 +21,14 @@ @move="$emit('move', $event)" @duplicate="$emit('duplicate')" /> - <component - :is="elementType.editComponent" - class="element__component" + <PageRootElement :element="element" :builder="builder" - /> + :mode="'editing'" + ></PageRootElement> <InsertElementButton v-if="active" - class="element__insert--bottom" + class="element-preview__insert--bottom" @click="$emit('insert', PLACEMENTS.AFTER)" /> </div> @@ -35,10 +37,12 @@ <script> import ElementMenu from '@baserow/modules/builder/components/elements/ElementMenu' import InsertElementButton from '@baserow/modules/builder/components/elements/InsertElementButton' +import PageRootElement from '@baserow/modules/builder/components/page/PageRootElement' import { PLACEMENTS } from '@baserow/modules/builder/enums' + export default { name: 'ElementPreview', - components: { ElementMenu, InsertElementButton }, + components: { ElementMenu, InsertElementButton, PageRootElement }, inject: ['builder'], props: { element: { diff --git a/web-frontend/modules/builder/components/elements/InsertElementButton.vue b/web-frontend/modules/builder/components/elements/InsertElementButton.vue index 82afeb59b..1f1084b4c 100644 --- a/web-frontend/modules/builder/components/elements/InsertElementButton.vue +++ b/web-frontend/modules/builder/components/elements/InsertElementButton.vue @@ -1,5 +1,5 @@ <template> - <a class="element__insert" @click="$emit('click')"> + <a class="element-preview__insert" @click="$emit('click')"> <i class="fas fa-plus"></i> </a> </template> diff --git a/web-frontend/modules/builder/components/elements/components/HeadingElement.vue b/web-frontend/modules/builder/components/elements/components/HeadingElement.vue index 1848b191f..90d024b8a 100644 --- a/web-frontend/modules/builder/components/elements/components/HeadingElement.vue +++ b/web-frontend/modules/builder/components/elements/components/HeadingElement.vue @@ -9,11 +9,11 @@ </template> <script> -import textElement from '@baserow/modules/builder/mixins/elements/textElement' +import element from '@baserow/modules/builder/mixins/element' export default { name: 'HeadingElement', - mixins: [textElement], + mixins: [element], props: { /** * @type {Object} diff --git a/web-frontend/modules/builder/components/elements/components/ImageElement.vue b/web-frontend/modules/builder/components/elements/components/ImageElement.vue index 0afbf4b86..c5910bc8f 100644 --- a/web-frontend/modules/builder/components/elements/components/ImageElement.vue +++ b/web-frontend/modules/builder/components/elements/components/ImageElement.vue @@ -9,10 +9,12 @@ </template> <script> +import element from '@baserow/modules/builder/mixins/element' import { IMAGE_SOURCE_TYPES } from '@baserow/modules/builder/enums' export default { name: 'ImageElement', + mixins: [element], props: { /** * @type {Object} @@ -36,6 +38,7 @@ export default { classes() { return { [`element--alignment-${this.element.alignment}`]: true, + 'element--no-value': !this.imageSource && !this.element.alt_text, } }, }, diff --git a/web-frontend/modules/builder/components/elements/components/LinkElement.vue b/web-frontend/modules/builder/components/elements/components/LinkElement.vue index 31751f1e4..97d4dc340 100644 --- a/web-frontend/modules/builder/components/elements/components/LinkElement.vue +++ b/web-frontend/modules/builder/components/elements/components/LinkElement.vue @@ -1,21 +1,15 @@ <template> <div class="link-element" :class="classes"> - <Button - v-if="element.variant === 'button'" - tag="a" - v-bind="extraAttr" - :target="element.target" - :full-width="element.width === 'full'" - @click="onClick($event)" - > - {{ element.value || $t('linkElement.noValue') }} - </Button> <a - v-else - class="link-element__link" + :class="{ + 'link-element__link': element.variant !== 'button', + 'link-element__button': element.variant === 'button', + 'link-element__button--full-width': + element.variant === 'button' && element.width === 'full', + }" v-bind="extraAttr" :target="`_${element.target}`" - @click="onClick($event)" + @click="onClick" > {{ element.value || $t('linkElement.noValue') }} </a> @@ -23,7 +17,7 @@ </template> <script> -import textElement from '@baserow/modules/builder/mixins/elements/textElement' +import element from '@baserow/modules/builder/mixins/element' import { LinkElementType } from '@baserow/modules/builder/elementTypes' /** @@ -40,18 +34,7 @@ import { LinkElementType } from '@baserow/modules/builder/elementTypes' export default { name: 'LinkElement', - mixins: [textElement], - props: { - /** - * @type {LinkElement} - */ - element: { - type: Object, - required: true, - }, - builder: { type: Object, required: true }, - mode: { type: String, required: true }, - }, + mixins: [element], computed: { classes() { return { @@ -100,6 +83,11 @@ export default { }, methods: { onClick(event) { + if (this.mode === 'editing') { + event.preventDefault() + return + } + if (!this.url) { event.preventDefault() } else if ( diff --git a/web-frontend/modules/builder/components/elements/components/LinkElementEdit.vue b/web-frontend/modules/builder/components/elements/components/LinkElementEdit.vue deleted file mode 100644 index def47eb11..000000000 --- a/web-frontend/modules/builder/components/elements/components/LinkElementEdit.vue +++ /dev/null @@ -1,65 +0,0 @@ -<template> - <div class="link-element" :class="classes"> - <Button - v-if="element.variant === 'button'" - tag="a" - v-bind="extraAttr" - :target="element.target" - :full-width="element.width === 'full'" - @click.prevent - > - {{ element.value || $t('linkElement.noValue') }} - </Button> - <a - v-else - class="link-element__link" - v-bind="extraAttr" - :target="`_${element.target}`" - @click.prevent - > - {{ element.value || $t('linkElement.noValue') }} - </a> - </div> -</template> - -<script> -import textElement from '@baserow/modules/builder/mixins/elements/textElement' -import { LinkElementType } from '@baserow/modules/builder/elementTypes' - -export default { - name: 'LinkElementEdit', - mixins: [textElement], - props: { - /** - * @type {LinkElement} - */ - element: { - type: Object, - required: true, - }, - builder: { type: Object, required: true }, - }, - computed: { - classes() { - return { - [`element--alignment-${this.element.alignment}`]: true, - 'element--no-value': !this.element.value, - } - }, - extraAttr() { - const attr = {} - if (this.url) { - attr.href = this.url - } - return attr - }, - url() { - try { - return LinkElementType.getUrlFromElement(this.element, this.builder) - } catch (e) { - return '' - } - }, - }, -} -</script> diff --git a/web-frontend/modules/builder/components/elements/components/ParagraphElement.vue b/web-frontend/modules/builder/components/elements/components/ParagraphElement.vue index 007d38165..ff5e1f39f 100644 --- a/web-frontend/modules/builder/components/elements/components/ParagraphElement.vue +++ b/web-frontend/modules/builder/components/elements/components/ParagraphElement.vue @@ -14,7 +14,7 @@ </template> <script> -import textElement from '@baserow/modules/builder/mixins/elements/textElement' +import element from '@baserow/modules/builder/mixins/element' import { generateHash } from '@baserow/modules/core/utils/hashing' /** @@ -25,7 +25,7 @@ import { generateHash } from '@baserow/modules/core/utils/hashing' export default { name: 'ParagraphElement', - mixins: [textElement], + mixins: [element], props: { /** * @type {Object} diff --git a/web-frontend/modules/builder/components/page/PageContent.vue b/web-frontend/modules/builder/components/page/PageContent.vue index 0404ed53b..0dff27888 100644 --- a/web-frontend/modules/builder/components/page/PageContent.vue +++ b/web-frontend/modules/builder/components/page/PageContent.vue @@ -1,11 +1,9 @@ <template> <div> - <component - :is="getType(element).component" + <PageRootElement v-for="element in elements" :key="element.id" :element="element" - class="element__component" :builder="builder" :mode="mode" /> @@ -13,7 +11,10 @@ </template> <script> +import PageRootElement from '@baserow/modules/builder/components/page/PageRootElement' + export default { + components: { PageRootElement }, inject: ['builder', 'mode'], props: { page: { @@ -33,10 +34,5 @@ export default { required: true, }, }, - methods: { - getType(element) { - return this.$registry.get('element', element.type) - }, - }, } </script> diff --git a/web-frontend/modules/builder/components/page/PageRootElement.vue b/web-frontend/modules/builder/components/page/PageRootElement.vue new file mode 100644 index 000000000..6498e323c --- /dev/null +++ b/web-frontend/modules/builder/components/page/PageRootElement.vue @@ -0,0 +1,53 @@ +<template functional> + <!-- + This element is supposed to be wrapping the root elements on a page. They allow + setting a width, background, borders, and more, but this only makes sense if they're + added to the root of the page. Child elements in for example a containing element must + not be wrapped by this component. + --> + <div + class="page-root-element" + :style="{ + 'padding-top': `${props.element.style_padding_top || 0}px`, + 'padding-bottom': `${props.element.style_padding_bottom || 0}px`, + }" + > + <div class="page-root-element__inner"> + <component + :is="$options.methods.getComponent(parent, props.element, props.mode)" + :element="props.element" + :builder="props.builder" + :mode="props.mode" + class="element" + /> + </div> + </div> +</template> + +<script> +export default { + name: 'PageRootElement', + props: { + element: { + type: Object, + required: true, + }, + builder: { + type: Object, + required: true, + }, + mode: { + type: String, + required: false, + default: '', + }, + }, + methods: { + getComponent(parent, element, mode) { + const elementType = parent.$registry.get('element', element.type) + const componentName = mode === 'editing' ? 'editComponent' : 'component' + return elementType[componentName] + }, + }, +} +</script> diff --git a/web-frontend/modules/builder/components/page/sidePanels/GeneralSidePanel.vue b/web-frontend/modules/builder/components/page/sidePanels/GeneralSidePanel.vue index bb474b746..7fef179ff 100644 --- a/web-frontend/modules/builder/components/page/sidePanels/GeneralSidePanel.vue +++ b/web-frontend/modules/builder/components/page/sidePanels/GeneralSidePanel.vue @@ -11,50 +11,10 @@ </template> <script> -import { mapActions, mapGetters } from 'vuex' -import { notifyIf } from '@baserow/modules/core/utils/error' -import { clone } from '@baserow/modules/core/utils/object' -import _ from 'lodash' +import elementSidePanel from '@baserow/modules/builder/mixins/elementSidePanel' export default { name: 'GeneralSidePanel', - inject: ['builder'], - computed: { - ...mapGetters({ - element: 'element/getSelected', - }), - - elementType() { - if (this.element) { - return this.$registry.get('element', this.element.type) - } - return null - }, - - defaultValues() { - return this.element - }, - }, - methods: { - ...mapActions({ - actionDebouncedUpdateSelectedElement: 'element/debouncedUpdateSelected', - }), - async onChange(newValues) { - const oldValues = this.element - if (!_.isEqual(newValues, oldValues)) { - try { - await this.actionDebouncedUpdateSelectedElement({ - // Here we clone the values to prevent - // "modification oustide of the store" error - values: clone(newValues), - }) - } catch (error) { - // Restore the previous saved values from the store - this.$refs.elementForm.reset() - notifyIf(error) - } - } - }, - }, + mixins: [elementSidePanel], } </script> diff --git a/web-frontend/modules/builder/components/page/sidePanels/StyleBoxForm.vue b/web-frontend/modules/builder/components/page/sidePanels/StyleBoxForm.vue new file mode 100644 index 000000000..2fbb62cac --- /dev/null +++ b/web-frontend/modules/builder/components/page/sidePanels/StyleBoxForm.vue @@ -0,0 +1,65 @@ +<template> + <form @submit.prevent> + <FormElement class="control"> + <label class="control__label">{{ label }}</label> + <div class="control__elements"> + <input + v-model="values[paddingName]" + type="number" + class="input" + :class="{ 'input--error': $v.values[paddingName].$error }" + @blur="$v.values[paddingName].$touch()" + /> + <div v-if="$v.values[paddingName].$error" class="error"> + {{ $t('styleBoxForm.paddingError') }} + </div> + </div> + </FormElement> + </form> +</template> + +<script> +import { required, integer, between } from 'vuelidate/lib/validators' +import form from '@baserow/modules/core/mixins/form' + +export default { + name: 'StyleBoxForm', + mixins: [form], + props: { + label: { + type: String, + required: true, + }, + paddingName: { + type: String, + required: true, + }, + }, + data() { + return { + allowedValues: [this.paddingName], + values: { + [this.paddingName]: 0, + }, + } + }, + validations() { + return { + values: { + [this.paddingName]: { + required, + integer, + between: between(0, 200), + }, + }, + } + }, + methods: { + emitChange(newValues) { + if (this.isFormValid()) { + this.$emit('values-changed', newValues) + } + }, + }, +} +</script> diff --git a/web-frontend/modules/builder/components/page/sidePanels/StyleSidePanel.vue b/web-frontend/modules/builder/components/page/sidePanels/StyleSidePanel.vue index 317043198..317b0d614 100644 --- a/web-frontend/modules/builder/components/page/sidePanels/StyleSidePanel.vue +++ b/web-frontend/modules/builder/components/page/sidePanels/StyleSidePanel.vue @@ -1,12 +1,27 @@ <template> - <div>Style panel</div> + <div :key="element.id"> + <StyleBoxForm + :label="$t('styleSidePanel.paddingTop')" + padding-name="style_padding_top" + :default-values="defaultValues" + @values-changed="onChange($event)" + ></StyleBoxForm> + <StyleBoxForm + :label="$t('styleSidePanel.paddingBottom')" + padding-name="style_padding_bottom" + :default-values="defaultValues" + @values-changed="onChange($event)" + ></StyleBoxForm> + </div> </template> <script> +import elementSidePanel from '@baserow/modules/builder/mixins/elementSidePanel' +import StyleBoxForm from '@baserow/modules/builder/components/page/sidePanels/StyleBoxForm.vue' + export default { name: 'StyleSidePanel', - data() { - return {} - }, + components: { StyleBoxForm }, + mixins: [elementSidePanel], } </script> diff --git a/web-frontend/modules/builder/elementTypes.js b/web-frontend/modules/builder/elementTypes.js index a628bb891..d493ac9f4 100644 --- a/web-frontend/modules/builder/elementTypes.js +++ b/web-frontend/modules/builder/elementTypes.js @@ -2,7 +2,6 @@ import { Registerable } from '@baserow/modules/core/registry' import ParagraphElement from '@baserow/modules/builder/components/elements/components/ParagraphElement' import HeadingElement from '@baserow/modules/builder/components/elements/components/HeadingElement' import LinkElement from '@baserow/modules/builder/components/elements/components/LinkElement' -import LinkElementEdit from '@baserow/modules/builder/components/elements/components/LinkElementEdit' import ParagraphElementForm from '@baserow/modules/builder/components/elements/components/forms/ParagraphElementForm' import HeadingElementForm from '@baserow/modules/builder/components/elements/components/forms/HeadingElementForm' import LinkElementForm from '@baserow/modules/builder/components/elements/components/forms/LinkElementForm' @@ -131,10 +130,6 @@ export class LinkElementType extends ElementType { return LinkElement } - get editComponent() { - return LinkElementEdit - } - get formComponent() { return LinkElementForm } diff --git a/web-frontend/modules/builder/mixins/element.js b/web-frontend/modules/builder/mixins/element.js new file mode 100644 index 000000000..617244c0e --- /dev/null +++ b/web-frontend/modules/builder/mixins/element.js @@ -0,0 +1,20 @@ +export default { + props: { + element: { + type: Object, + required: true, + }, + builder: { + type: Object, + required: true, + }, + mode: { + // editing = being editing by the page editor + // preview = previewing the application + // public = publicly published application + type: String, + required: false, + default: '', + }, + }, +} diff --git a/web-frontend/modules/builder/mixins/elementSidePanel.js b/web-frontend/modules/builder/mixins/elementSidePanel.js new file mode 100644 index 000000000..0bea524f8 --- /dev/null +++ b/web-frontend/modules/builder/mixins/elementSidePanel.js @@ -0,0 +1,46 @@ +import { mapActions, mapGetters } from 'vuex' +import _ from 'lodash' + +import { clone } from '@baserow/modules/core/utils/object' +import { notifyIf } from '@baserow/modules/core/utils/error' + +export default { + inject: ['builder'], + computed: { + ...mapGetters({ + element: 'element/getSelected', + }), + + elementType() { + if (this.element) { + return this.$registry.get('element', this.element.type) + } + return null + }, + + defaultValues() { + return this.element + }, + }, + methods: { + ...mapActions({ + actionDebouncedUpdateSelectedElement: 'element/debouncedUpdateSelected', + }), + async onChange(newValues) { + const oldValues = this.element + if (!_.isEqual(newValues, oldValues)) { + try { + await this.actionDebouncedUpdateSelectedElement({ + // Here we clone the values to prevent + // "modification oustide of the store" error + values: clone(newValues), + }) + } catch (error) { + // Restore the previous saved values from the store + this.$refs.elementForm.reset() + notifyIf(error) + } + } + }, + }, +} diff --git a/web-frontend/modules/builder/mixins/elements/textElement.js b/web-frontend/modules/builder/mixins/elements/textElement.js deleted file mode 100644 index e1786ee30..000000000 --- a/web-frontend/modules/builder/mixins/elements/textElement.js +++ /dev/null @@ -1,9 +0,0 @@ -export default { - props: { - value: { - type: String, - required: false, - default: 'Some temp default value', - }, - }, -} diff --git a/web-frontend/modules/builder/pages/publicPage.vue b/web-frontend/modules/builder/pages/publicPage.vue index be73f89a9..d4f14a43e 100644 --- a/web-frontend/modules/builder/pages/publicPage.vue +++ b/web-frontend/modules/builder/pages/publicPage.vue @@ -72,5 +72,14 @@ export default { mode, } }, + head() { + return { + titleTemplate: '', + title: this.page.name, + bodyAttrs: { + class: 'public-page', + }, + } + }, } </script> diff --git a/web-frontend/modules/core/assets/scss/components/builder/all.scss b/web-frontend/modules/core/assets/scss/components/builder/all.scss index c6732d7ae..2e3f6faf6 100644 --- a/web-frontend/modules/core/assets/scss/components/builder/all.scss +++ b/web-frontend/modules/core/assets/scss/components/builder/all.scss @@ -5,9 +5,11 @@ @import 'add_element_modal'; @import 'page_preview'; @import 'element'; +@import 'element_preview'; @import 'side_panels'; @import 'empty_side_panel_state'; -@import 'page'; +@import 'page_editor'; +@import 'page_root_element'; @import 'page_settings_path_params_form_element'; @import 'domain_card'; @import 'dns_status'; @@ -15,5 +17,6 @@ @import 'integration_settings'; @import 'last_published_domain_date'; @import 'publish_action_modal'; +@import 'public_page'; @import 'data_source_context'; @import 'data_source_form'; diff --git a/web-frontend/modules/core/assets/scss/components/builder/element.scss b/web-frontend/modules/core/assets/scss/components/builder/element.scss index 2cf78c340..c34329ede 100644 --- a/web-frontend/modules/core/assets/scss/components/builder/element.scss +++ b/web-frontend/modules/core/assets/scss/components/builder/element.scss @@ -1,181 +1,19 @@ -.element__menu { - display: flex; - flex-wrap: nowrap; - border: solid 1px $color-neutral-400; - border-radius: 3px; - z-index: 2; - - @include absolute(5px, 5px, auto, auto); -} - -.element__insert { - @include center-text(26px, 10px); - - display: block; - border-radius: 100%; - border: solid 1px $color-neutral-300; - box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.16); - color: $color-primary-900; - background-color: $white; - - &:hover { - background-color: $color-neutral-50; - box-shadow: 0 3px 5px 0 rgba(0, 0, 0, 0.32); - } - - &--top, - &--bottom { - @include absolute(-13px, auto, auto, 50%); - - margin-left: -12px; - z-index: 2; - } - - &--bottom { - top: auto; - bottom: -12px; - } -} - .element { - position: relative; - - .element__insert { - display: none; - } - - .element__menu { - display: none; - } - - &:hover { - .element__insert { - display: block; - } - - .element__menu { - display: flex; - } - } - - &:not(.element--active) { - cursor: pointer; - } - - &.element--active { - cursor: inherit; - - &::before { - @include absolute(0, 0, 0, 0); - - content: ''; - border: solid 1px $color-primary-500; - pointer-events: none; - } - - .element__insert { - display: block; - } - } -} - -.element__menu-item-description { - @include absolute(-25px, -2px, auto, auto); - - display: none; - background-color: $color-neutral-900; - font-size: 11px; - color: $white; - line-height: 20px; - padding: 0 4px; - border-radius: 3px; - white-space: nowrap; -} - -.element__menu-item { - @include center-text(24px, 9px); - - position: relative; - background-color: $white; - color: $color-primary-900; - - &:hover { - background-color: $color-neutral-100; - - .element__menu-item-description { - display: block; - } - } - - &:first-child { - border-top-left-radius: 3px; - border-bottom-left-radius: 3px; - } - - &:last-child { - border-top-right-radius: 3px; - border-bottom-right-radius: 3px; - } - - &.disabled { - cursor: inherit; - color: $color-neutral-300; - - &:hover { - background-color: $white; - } - } -} - -.element__component { - margin: 0; -} - -.element__menu-duplicate-loading { - margin: 5px; + // this is a placeholder, the class will be added to every element component. } .element--no-value { opacity: 0.3; } -.element--in-error::after { - @extend .fas; - - @include fa-icon; - @include absolute(0, 0, auto, auto); - - content: fa-content($fa-var-exclamation-circle); - pointer-events: none; - width: 20px; - height: 20px; - line-height: 20px; - font-size: 20px; - margin-right: 5px; - margin-top: 5px; - color: $color-error-300; -} - .element--alignment-left { justify-content: start; - - .button--full-width { - text-align: left; - } } .element--alignment-center { justify-content: center; - - .button--full-width { - text-align: center; - } } .element--alignment-right { justify-content: end; - - .button--full-width { - text-align: right; - } } diff --git a/web-frontend/modules/core/assets/scss/components/builder/element_preview.scss b/web-frontend/modules/core/assets/scss/components/builder/element_preview.scss new file mode 100644 index 000000000..e3f6d8b31 --- /dev/null +++ b/web-frontend/modules/core/assets/scss/components/builder/element_preview.scss @@ -0,0 +1,149 @@ +.element-preview__menu { + display: flex; + flex-wrap: nowrap; + border: solid 1px $color-neutral-400; + border-radius: 3px; + z-index: 2; + + @include absolute(5px, 5px, auto, auto); +} + +.element-preview__insert { + @include center-text(26px, 10px); + + display: block; + border-radius: 100%; + border: solid 1px $color-neutral-300; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.16); + color: $color-primary-900; + background-color: $white; + + &:hover { + background-color: $color-neutral-50; + box-shadow: 0 3px 5px 0 rgba(0, 0, 0, 0.32); + } + + &--top, + &--bottom { + @include absolute(-13px, auto, auto, 50%); + + margin-left: -12px; + z-index: 2; + } + + &--bottom { + top: auto; + bottom: -12px; + } +} + +.element-preview { + position: relative; + + .element-preview__insert { + display: none; + } + + .element-preview__menu { + display: none; + } + + &:hover { + .element-preview__insert { + display: block; + } + + .element-preview__menu { + display: flex; + } + } + + &:not(.element-preview--active) { + cursor: pointer; + } + + &.element-preview--active { + cursor: inherit; + + &::before { + @include absolute(0, 0, 0, 0); + + content: ''; + border: solid 1px $color-primary-500; + pointer-events: none; + } + + .element-preview__insert { + display: block; + } + } +} + +.element-preview__menu-item-description { + @include absolute(-25px, -2px, auto, auto); + + display: none; + background-color: $color-neutral-900; + font-size: 11px; + color: $white; + line-height: 20px; + padding: 0 4px; + border-radius: 3px; + white-space: nowrap; +} + +.element-preview__menu-item { + @include center-text(24px, 9px); + + position: relative; + background-color: $white; + color: $color-primary-900; + + &:hover { + background-color: $color-neutral-100; + + .element-preview__menu-item-description { + display: block; + } + } + + &:first-child { + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; + } + + &:last-child { + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + } + + &.disabled { + cursor: inherit; + color: $color-neutral-300; + + &:hover { + background-color: $white; + } + } +} + +.element-preview__menu-duplicate-loading { + margin: 5px; +} + +.element-preview--in-error::after { + @extend .fas; + + @include fa-icon; + @include absolute(0, 0, auto, auto); + + content: fa-content($fa-var-exclamation-circle); + pointer-events: none; + width: 20px; + height: 20px; + line-height: 20px; + font-size: 20px; + margin-right: 5px; + margin-top: 5px; + color: $color-error-300; +} diff --git a/web-frontend/modules/core/assets/scss/components/builder/elements/forms/all.scss b/web-frontend/modules/core/assets/scss/components/builder/elements/forms/all.scss index ed7eefbd6..37769fc4d 100644 --- a/web-frontend/modules/core/assets/scss/components/builder/elements/forms/all.scss +++ b/web-frontend/modules/core/assets/scss/components/builder/elements/forms/all.scss @@ -1,2 +1,2 @@ -@import 'paragraphElementForm'; -@import 'linkElementForm'; +@import 'paragraph_element_form'; +@import 'link_element_form'; diff --git a/web-frontend/modules/core/assets/scss/components/builder/elements/forms/linkElementForm.scss b/web-frontend/modules/core/assets/scss/components/builder/elements/forms/link_element_form.scss similarity index 63% rename from web-frontend/modules/core/assets/scss/components/builder/elements/forms/linkElementForm.scss rename to web-frontend/modules/core/assets/scss/components/builder/elements/forms/link_element_form.scss index 0d21a82ec..9f5ba9c0a 100644 --- a/web-frontend/modules/core/assets/scss/components/builder/elements/forms/linkElementForm.scss +++ b/web-frontend/modules/core/assets/scss/components/builder/elements/forms/link_element_form.scss @@ -10,3 +10,11 @@ color: $color-neutral-500; margin-left: 5px; } + +.link-element-form__params-error { + display: flex; + flex-direction: column; + gap: 10px; + text-align: center; + align-items: center; +} diff --git a/web-frontend/modules/core/assets/scss/components/builder/elements/forms/paragraphElementForm.scss b/web-frontend/modules/core/assets/scss/components/builder/elements/forms/paragraphElementForm.scss deleted file mode 100644 index e366adf8b..000000000 --- a/web-frontend/modules/core/assets/scss/components/builder/elements/forms/paragraphElementForm.scss +++ /dev/null @@ -1,12 +0,0 @@ -.paragraph-element-form__value { - resize: vertical; - color: $color-primary-900; -} - -.link-element-form__params-error { - display: flex; - flex-direction: column; - gap: 10px; - text-align: center; - align-items: center; -} diff --git a/web-frontend/modules/core/assets/scss/components/builder/elements/forms/paragraph_element_form.scss b/web-frontend/modules/core/assets/scss/components/builder/elements/forms/paragraph_element_form.scss new file mode 100644 index 000000000..79f38b144 --- /dev/null +++ b/web-frontend/modules/core/assets/scss/components/builder/elements/forms/paragraph_element_form.scss @@ -0,0 +1,4 @@ +.paragraph-element-form__value { + resize: vertical; + color: $color-primary-900; +} diff --git a/web-frontend/modules/core/assets/scss/components/builder/elements/heading_element.scss b/web-frontend/modules/core/assets/scss/components/builder/elements/heading_element.scss index 3389a8a09..4a2343d96 100644 --- a/web-frontend/modules/core/assets/scss/components/builder/elements/heading_element.scss +++ b/web-frontend/modules/core/assets/scss/components/builder/elements/heading_element.scss @@ -1,29 +1,29 @@ +.heading-element { + margin: 0; + color: $black; +} + h1.heading-element { - font-size: 24px; - padding: 32px 82px; + font-size: 30px; } h2.heading-element { - font-size: 20px; - padding: 28px 82px; + font-size: 26px; } h3.heading-element { - font-size: 18px; - padding: 24px 82px; + font-size: 22px; } h4.heading-element { - font-size: 16px; - padding: 20px 82px; + font-size: 18px; } h5.heading-element { - font-size: 15px; - padding: 18px 82px; + font-size: 14px; } h6.heading-element { font-size: 14px; - padding: 16px 82px; + font-style: italic; } diff --git a/web-frontend/modules/core/assets/scss/components/builder/elements/link_element.scss b/web-frontend/modules/core/assets/scss/components/builder/elements/link_element.scss index b7a582ae0..3956852f9 100644 --- a/web-frontend/modules/core/assets/scss/components/builder/elements/link_element.scss +++ b/web-frontend/modules/core/assets/scss/components/builder/elements/link_element.scss @@ -1,13 +1,45 @@ .link-element { display: flex; - padding: 5px 82px; } .link-element__link { font-size: 14px; - font-weight: 700; - padding: 0; - height: 32px; - line-height: 32px; - border: 1px solid transparent; + color: $black; + text-decoration: underline; +} + +.link-element__button { + font-size: 14px; + cursor: pointer; + display: inline-block; + color: $white; + background-color: $black; + line-height: 28px; + padding: 0 12px; + border-radius: 3px; + border: none; + white-space: nowrap; + text-align: left; + text-decoration: none; + + &:hover { + background-color: lighten($black, 10%); + text-decoration: none; + } + + &:focus { + background-color: lighten($black, 20%); + } + + &--full-width { + width: 100%; + } + + .element--alignment-center & { + text-align: center; + } + + .element--alignment-right & { + text-align: right; + } } diff --git a/web-frontend/modules/core/assets/scss/components/builder/elements/paragraph_element.scss b/web-frontend/modules/core/assets/scss/components/builder/elements/paragraph_element.scss index 600604eef..b095e5f1c 100644 --- a/web-frontend/modules/core/assets/scss/components/builder/elements/paragraph_element.scss +++ b/web-frontend/modules/core/assets/scss/components/builder/elements/paragraph_element.scss @@ -1,5 +1,5 @@ .paragraph-element { font-size: 14px; - padding: 2px 82px; margin: 0; + color: $black; } diff --git a/web-frontend/modules/core/assets/scss/components/builder/page.scss b/web-frontend/modules/core/assets/scss/components/builder/page_editor.scss similarity index 100% rename from web-frontend/modules/core/assets/scss/components/builder/page.scss rename to web-frontend/modules/core/assets/scss/components/builder/page_editor.scss diff --git a/web-frontend/modules/core/assets/scss/components/builder/page_root_element.scss b/web-frontend/modules/core/assets/scss/components/builder/page_root_element.scss new file mode 100644 index 000000000..4d0f2774c --- /dev/null +++ b/web-frontend/modules/core/assets/scss/components/builder/page_root_element.scss @@ -0,0 +1,5 @@ +.page-root-element__inner { + padding: 0 20px; + margin: 0 auto; + max-width: $builder-page-max-width; +} diff --git a/web-frontend/modules/core/assets/scss/components/builder/public_page.scss b/web-frontend/modules/core/assets/scss/components/builder/public_page.scss new file mode 100644 index 000000000..138c92b1b --- /dev/null +++ b/web-frontend/modules/core/assets/scss/components/builder/public_page.scss @@ -0,0 +1,3 @@ +.public-page { + background-color: $white; +} diff --git a/web-frontend/modules/core/assets/scss/variables.scss b/web-frontend/modules/core/assets/scss/variables.scss index 95b242418..04adbcaa4 100644 --- a/web-frontend/modules/core/assets/scss/variables.scss +++ b/web-frontend/modules/core/assets/scss/variables.scss @@ -102,3 +102,5 @@ $file-field-modal-body-nav-width: 120px !default; $file-field-modal-foot-height: 108px !default; $dashboard-breakpoint: 1100px; + +$builder-page-max-width: 1280px; diff --git a/web-frontend/modules/core/locales/en.json b/web-frontend/modules/core/locales/en.json index 4b151cf7c..6db557c4c 100644 --- a/web-frontend/modules/core/locales/en.json +++ b/web-frontend/modules/core/locales/en.json @@ -524,5 +524,12 @@ }, "dropdown": { "empty": "No items available" + }, + "styleSidePanel": { + "paddingTop": "Padding top", + "paddingBottom": "Padding bottom" + }, + "styleBoxForm": { + "paddingError": "The value must be an integer between 0 and 200." } }