mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-05-12 04:11:49 +00:00
Add Link element
This commit is contained in:
parent
7309f56ad5
commit
e45fd10652
49 changed files with 1484 additions and 249 deletions
backend/src/baserow/contrib/builder
web-frontend
modules
builder
components
elementTypes.jslocales
pages
plugin.jsservices
store
utils
core
test/unit/builder
|
@ -80,3 +80,8 @@ class MoveElementSerializer(serializers.Serializer):
|
|||
"Otherwise the element is placed at the end of the page."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class PageParameterValueSerializer(serializers.Serializer):
|
||||
name = serializers.CharField()
|
||||
value = ExpressionField(allow_blank=True)
|
||||
|
|
|
@ -101,11 +101,16 @@ class BuilderConfig(AppConfig):
|
|||
|
||||
permission_manager_type_registry.register(AllowPublicBuilderManagerType())
|
||||
|
||||
from .elements.element_types import HeadingElementType, ParagraphElementType
|
||||
from .elements.element_types import (
|
||||
HeadingElementType,
|
||||
LinkElementType,
|
||||
ParagraphElementType,
|
||||
)
|
||||
from .elements.registries import element_type_registry
|
||||
|
||||
element_type_registry.register(HeadingElementType())
|
||||
element_type_registry.register(ParagraphElementType())
|
||||
element_type_registry.register(LinkElementType())
|
||||
|
||||
from .domains.trash_types import DomainTrashableItemType
|
||||
|
||||
|
|
|
@ -2,7 +2,11 @@ from abc import ABC
|
|||
|
||||
from rest_framework import serializers
|
||||
|
||||
from baserow.contrib.builder.elements.models import HeadingElement, ParagraphElement
|
||||
from baserow.contrib.builder.elements.models import (
|
||||
HeadingElement,
|
||||
LinkElement,
|
||||
ParagraphElement,
|
||||
)
|
||||
from baserow.contrib.builder.elements.registries import ElementType
|
||||
from baserow.contrib.builder.elements.types import Expression
|
||||
from baserow.contrib.builder.types import ElementDict
|
||||
|
@ -89,3 +93,116 @@ class ParagraphElementType(BaseTextElementType):
|
|||
"Asperiores corporis perspiciatis nam harum veritatis. "
|
||||
"Impedit qui maxime aut illo quod ea molestias."
|
||||
}
|
||||
|
||||
|
||||
class LinkElementType(BaseTextElementType):
|
||||
"""
|
||||
A simple paragraph element that can be used to display a paragraph of text.
|
||||
"""
|
||||
|
||||
type = "link"
|
||||
model_class = LinkElement
|
||||
|
||||
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"]:
|
||||
serialized_copy["navigate_to_page_id"] = id_mapping["builder_pages"][
|
||||
serialized_copy["navigate_to_page_id"]
|
||||
]
|
||||
return super().import_serialized(page, serialized_copy, id_mapping)
|
||||
|
||||
@property
|
||||
def serializer_field_overrides(self):
|
||||
from baserow.contrib.builder.api.elements.serializers import (
|
||||
ExpressionField,
|
||||
PageParameterValueSerializer,
|
||||
)
|
||||
|
||||
overrides = {
|
||||
"navigation_type": serializers.ChoiceField(
|
||||
choices=LinkElement.NAVIGATION_TYPES.choices,
|
||||
help_text=LinkElement._meta.get_field("navigation_type").help_text,
|
||||
required=False,
|
||||
),
|
||||
"navigate_to_page_id": serializers.IntegerField(
|
||||
allow_null=True,
|
||||
help_text=LinkElement._meta.get_field("navigate_to_page").help_text,
|
||||
required=False,
|
||||
),
|
||||
"navigate_to_url": ExpressionField(
|
||||
help_text=LinkElement._meta.get_field("navigate_to_url").help_text,
|
||||
default="",
|
||||
allow_blank=True,
|
||||
required=False,
|
||||
),
|
||||
"page_parameters": PageParameterValueSerializer(
|
||||
many=True,
|
||||
help_text=LinkElement._meta.get_field("navigate_to_url").help_text,
|
||||
required=False,
|
||||
),
|
||||
"variant": serializers.ChoiceField(
|
||||
choices=LinkElement.VARIANTS.choices,
|
||||
help_text=LinkElement._meta.get_field("variant").help_text,
|
||||
required=False,
|
||||
),
|
||||
"target": serializers.ChoiceField(
|
||||
choices=LinkElement.TARGETS.choices,
|
||||
help_text=LinkElement._meta.get_field("target").help_text,
|
||||
required=False,
|
||||
),
|
||||
"width": serializers.ChoiceField(
|
||||
choices=LinkElement.WIDTHS.choices,
|
||||
help_text=LinkElement._meta.get_field("width").help_text,
|
||||
required=False,
|
||||
),
|
||||
"alignment": serializers.ChoiceField(
|
||||
choices=LinkElement.ALIGNMENTS.choices,
|
||||
help_text=LinkElement._meta.get_field("alignment").help_text,
|
||||
required=False,
|
||||
),
|
||||
}
|
||||
overrides.update(super().serializer_field_overrides)
|
||||
return overrides
|
||||
|
||||
def get_sample_params(self):
|
||||
return {
|
||||
"navigation_type": "custom",
|
||||
"navigate_to_page_id": None,
|
||||
"navigate_to_url": "http://example.com",
|
||||
"page_parameters": [],
|
||||
"variant": "link",
|
||||
"target": "blank",
|
||||
"width": "auto",
|
||||
"alignment": "center",
|
||||
}
|
||||
|
|
|
@ -111,6 +111,8 @@ class ElementHandler:
|
|||
kwargs, shared_allowed_fields + element_type.allowed_fields
|
||||
)
|
||||
|
||||
allowed_values = element_type.prepare_value_for_db(allowed_values)
|
||||
|
||||
model_class = cast(Element, element_type.model_class)
|
||||
|
||||
element = model_class(page=page, order=order, **allowed_values)
|
||||
|
@ -144,6 +146,8 @@ class ElementHandler:
|
|||
kwargs, shared_allowed_fields + element_type.allowed_fields
|
||||
)
|
||||
|
||||
allowed_updates = element_type.prepare_value_for_db(allowed_updates)
|
||||
|
||||
for key, value in allowed_updates.items():
|
||||
setattr(element, key, value)
|
||||
|
||||
|
|
|
@ -116,3 +116,75 @@ class ParagraphElement(BaseTextElement):
|
|||
"""
|
||||
A simple paragraph.
|
||||
"""
|
||||
|
||||
|
||||
class LinkElement(BaseTextElement):
|
||||
"""
|
||||
A simple link.
|
||||
"""
|
||||
|
||||
class NAVIGATION_TYPES(models.TextChoices):
|
||||
PAGE = "page"
|
||||
CUSTOM = "custom"
|
||||
|
||||
class VARIANTS(models.TextChoices):
|
||||
LINK = "link"
|
||||
BUTTON = "button"
|
||||
|
||||
class TARGETS(models.TextChoices):
|
||||
SELF = "self"
|
||||
BLANK = "blank"
|
||||
|
||||
class ALIGNMENTS(models.TextChoices):
|
||||
LEFT = "left"
|
||||
CENTER = "center"
|
||||
RIGHT = "right"
|
||||
|
||||
class WIDTHS(models.TextChoices):
|
||||
AUTO = "auto"
|
||||
FULL = "full"
|
||||
|
||||
navigation_type = models.CharField(
|
||||
choices=NAVIGATION_TYPES.choices,
|
||||
help_text="The navigation type.",
|
||||
max_length=10,
|
||||
default=NAVIGATION_TYPES.PAGE,
|
||||
)
|
||||
navigate_to_page = models.ForeignKey(
|
||||
Page,
|
||||
null=True,
|
||||
on_delete=models.SET_NULL,
|
||||
help_text=(
|
||||
"Destination page id for this link. If null then we use the "
|
||||
"navigate_to_url property instead.",
|
||||
),
|
||||
)
|
||||
navigate_to_url = ExpressionField(
|
||||
default="",
|
||||
help_text="If no page is selected, this indicate the destination of the link.",
|
||||
)
|
||||
page_parameters = models.JSONField(
|
||||
default=list,
|
||||
help_text="The parameters for each parameters of the selected page if any.",
|
||||
)
|
||||
|
||||
variant = models.CharField(
|
||||
choices=VARIANTS.choices,
|
||||
help_text="The variant of the link.",
|
||||
max_length=10,
|
||||
default=VARIANTS.LINK,
|
||||
)
|
||||
target = models.CharField(
|
||||
choices=TARGETS.choices,
|
||||
help_text="The target of the link when we click on it.",
|
||||
max_length=10,
|
||||
default=TARGETS.SELF,
|
||||
)
|
||||
width = models.CharField(
|
||||
choices=WIDTHS.choices,
|
||||
max_length=10,
|
||||
default=WIDTHS.AUTO,
|
||||
)
|
||||
alignment = models.CharField(
|
||||
choices=ALIGNMENTS.choices, max_length=10, default=ALIGNMENTS.LEFT
|
||||
)
|
||||
|
|
|
@ -29,6 +29,9 @@ class ElementType(
|
|||
|
||||
SerializedDict: Type[ElementDictSubClass]
|
||||
|
||||
def prepare_value_for_db(self, values):
|
||||
return values
|
||||
|
||||
def export_serialized(
|
||||
self,
|
||||
element: Element,
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
# Generated by Django 3.2.18 on 2023-04-06 13:19
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
import baserow.contrib.builder.elements.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("builder", "0009_add_domain_publish"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="LinkElement",
|
||||
fields=[
|
||||
(
|
||||
"element_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="builder.element",
|
||||
),
|
||||
),
|
||||
(
|
||||
"value",
|
||||
baserow.contrib.builder.elements.models.ExpressionField(default=""),
|
||||
),
|
||||
(
|
||||
"navigation_type",
|
||||
models.CharField(
|
||||
choices=[("page", "Page"), ("custom", "Custom")],
|
||||
default="page",
|
||||
help_text="The navigation type.",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
(
|
||||
"navigate_to_url",
|
||||
baserow.contrib.builder.elements.models.ExpressionField(
|
||||
default="",
|
||||
help_text="If no page is selected, this indicate the destination of the link.",
|
||||
),
|
||||
),
|
||||
(
|
||||
"page_parameters",
|
||||
models.JSONField(
|
||||
default=list,
|
||||
help_text="The parameters for each parameters of the selected page if any.",
|
||||
),
|
||||
),
|
||||
(
|
||||
"variant",
|
||||
models.CharField(
|
||||
choices=[("link", "Link"), ("button", "Button")],
|
||||
default="link",
|
||||
help_text="The variant of the link.",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
(
|
||||
"target",
|
||||
models.CharField(
|
||||
choices=[("self", "Self"), ("blank", "Blank")],
|
||||
default="self",
|
||||
help_text="The target of the link when we click on it.",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
(
|
||||
"width",
|
||||
models.CharField(
|
||||
choices=[("auto", "Auto"), ("full", "Full")],
|
||||
default="auto",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
(
|
||||
"alignment",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("left", "Left"),
|
||||
("center", "Center"),
|
||||
("right", "Right"),
|
||||
],
|
||||
default="left",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
(
|
||||
"navigate_to_page",
|
||||
models.ForeignKey(
|
||||
help_text=(
|
||||
"Destination page id for this link. If null then we use the navigate_to_url property instead.",
|
||||
),
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="builder.page",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
bases=("builder.element",),
|
||||
),
|
||||
]
|
|
@ -29,7 +29,11 @@ def load_test_data():
|
|||
)
|
||||
|
||||
Domain.objects.filter(domain_name="test1.getbaserow.io").delete()
|
||||
Domain.objects.filter(domain_name="test2.getbaserow.io").delete()
|
||||
Domain.objects.filter(domain_name="test3.getbaserow.io").delete()
|
||||
Domain.objects.create(builder=builder, domain_name="test1.getbaserow.io", order=1)
|
||||
Domain.objects.create(builder=builder, domain_name="test2.getbaserow.io", order=2)
|
||||
Domain.objects.create(builder=builder, domain_name="test3.getbaserow.io", order=3)
|
||||
|
||||
try:
|
||||
homepage = Page.objects.get(name="Homepage", builder=builder)
|
||||
|
@ -38,6 +42,7 @@ def load_test_data():
|
|||
|
||||
heading_element_type = element_type_registry.get("heading")
|
||||
paragraph_element_type = element_type_registry.get("paragraph")
|
||||
link_element_type = element_type_registry.get("link")
|
||||
|
||||
ElementHandler().create_element(
|
||||
heading_element_type, homepage, value="Back to local", level=1
|
||||
|
@ -124,3 +129,35 @@ def load_test_data():
|
|||
"sunt in culpa qui officia deserunt mollit anim id est laborum."
|
||||
),
|
||||
)
|
||||
|
||||
ElementHandler().create_element(
|
||||
link_element_type,
|
||||
terms,
|
||||
value="Home",
|
||||
variant="button",
|
||||
alignment="right",
|
||||
navigation_type="page",
|
||||
navigate_to_page=homepage,
|
||||
)
|
||||
|
||||
# Button for homepage
|
||||
ElementHandler().create_element(
|
||||
link_element_type,
|
||||
homepage,
|
||||
value="See terms",
|
||||
variant="button",
|
||||
alignment="right",
|
||||
navigation_type="page",
|
||||
navigate_to_page=terms,
|
||||
)
|
||||
|
||||
ElementHandler().create_element(
|
||||
link_element_type,
|
||||
homepage,
|
||||
value="Visit Baserow",
|
||||
variant="link",
|
||||
alignment="center",
|
||||
navigation_type="custom",
|
||||
target="blank",
|
||||
navigate_to_url="https://baserow.io",
|
||||
)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div
|
||||
class="element"
|
||||
:class="{ 'element--active': active }"
|
||||
:class="{ 'element--active': active, 'element--in-error': inError }"
|
||||
@click="$emit('selected')"
|
||||
>
|
||||
<InsertElementButton
|
||||
|
@ -19,10 +19,11 @@
|
|||
@duplicate="$emit('duplicate')"
|
||||
/>
|
||||
<component
|
||||
:is="elementType.component"
|
||||
v-bind="elementType.getComponentProps(element)"
|
||||
:is="elementType.editComponent"
|
||||
class="element__component"
|
||||
></component>
|
||||
:element="element"
|
||||
:builder="builder"
|
||||
/>
|
||||
<InsertElementButton
|
||||
v-if="active"
|
||||
class="element__insert--bottom"
|
||||
|
@ -38,6 +39,7 @@ import { PLACEMENTS } from '@baserow/modules/builder/enums'
|
|||
export default {
|
||||
name: 'ElementPreview',
|
||||
components: { ElementMenu, InsertElementButton },
|
||||
inject: ['builder'],
|
||||
props: {
|
||||
element: {
|
||||
type: Object,
|
||||
|
@ -69,6 +71,12 @@ export default {
|
|||
elementType() {
|
||||
return this.$registry.get('element', this.element.type)
|
||||
},
|
||||
inError() {
|
||||
return this.elementType.isInError({
|
||||
element: this.element,
|
||||
builder: this.builder,
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<component
|
||||
:is="`h${level}`"
|
||||
:is="`h${element.level}`"
|
||||
class="heading-element"
|
||||
:class="{ 'element--no-value': !value }"
|
||||
:class="{ 'element--no-value': !element.value }"
|
||||
>
|
||||
{{ value || $t('headingElement.noValue') }}
|
||||
{{ element.value || $t('headingElement.noValue') }}
|
||||
</component>
|
||||
</template>
|
||||
|
||||
|
@ -15,8 +15,8 @@ export default {
|
|||
name: 'HeaderElement',
|
||||
mixins: [textElement],
|
||||
props: {
|
||||
level: {
|
||||
type: Number,
|
||||
element: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
<template>
|
||||
<div class="link-element" :class="classes">
|
||||
<Button
|
||||
v-if="element.variant === 'button'"
|
||||
type="link"
|
||||
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"
|
||||
v-bind="extraAttr"
|
||||
:target="`_${element.target}`"
|
||||
@click="onClick($event)"
|
||||
>
|
||||
{{ 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'
|
||||
|
||||
/**
|
||||
* @typedef LinkElement
|
||||
* @property {string} value The text inside the button
|
||||
* @property {string} alignment left|center|right
|
||||
* @property {string} variant link|button
|
||||
* @property {string} navigation_type page|custom
|
||||
* @property {string} navigate_to_page_id The page id for `page` navigation type.
|
||||
* @property {object} page_parameters the page paramaters
|
||||
* @property {string} navigate_to_url The URL for `custom` navigation type.
|
||||
* @property {string} target self|blank
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: 'LinkElement',
|
||||
mixins: [textElement],
|
||||
props: {
|
||||
/**
|
||||
* @type {LinkElement}
|
||||
*/
|
||||
element: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
builder: { type: Object, required: true },
|
||||
mode: { type: String, required: true },
|
||||
},
|
||||
computed: {
|
||||
classes() {
|
||||
return {
|
||||
[`link-element--alignment-${this.element.alignment}`]: true,
|
||||
'element--no-value': !this.element.value,
|
||||
}
|
||||
},
|
||||
extraAttr() {
|
||||
const attr = {}
|
||||
if (this.url) {
|
||||
attr.href = this.url
|
||||
}
|
||||
if (this.isExternalLink) {
|
||||
attr.rel = 'noopener noreferrer'
|
||||
}
|
||||
return attr
|
||||
},
|
||||
originalUrl() {
|
||||
try {
|
||||
return LinkElementType.getUrlFromElement(this.element, this.builder)
|
||||
} catch (e) {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
url() {
|
||||
if (
|
||||
this.originalUrl &&
|
||||
this.mode === 'preview' &&
|
||||
(this.element.navigation_type === 'page' ||
|
||||
(this.element.navigation_type === 'custom' &&
|
||||
this.originalUrl.startsWith('/')))
|
||||
) {
|
||||
// Add prefix in preview mode for page navigation or custom URL starting
|
||||
// with `/`
|
||||
return `/builder/${this.builder.id}/preview${this.originalUrl}`
|
||||
} else {
|
||||
return this.originalUrl
|
||||
}
|
||||
},
|
||||
isExternalLink() {
|
||||
return (
|
||||
this.element.navigation_type === 'custom' &&
|
||||
!this.originalUrl.startsWith('/')
|
||||
)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClick(event) {
|
||||
if (!this.url) {
|
||||
event.preventDefault()
|
||||
} else if (
|
||||
this.element.navigation_type === 'page' &&
|
||||
this.element.target !== 'blank'
|
||||
) {
|
||||
event.preventDefault()
|
||||
this.$router.push(this.url)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,65 @@
|
|||
<template>
|
||||
<div class="link-element" :class="classes">
|
||||
<Button
|
||||
v-if="element.variant === 'button'"
|
||||
type="link"
|
||||
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 {
|
||||
[`link-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>
|
|
@ -20,9 +20,15 @@ import { generateHash } from '@baserow/modules/core/utils/hashing'
|
|||
export default {
|
||||
name: 'ParagraphElement',
|
||||
mixins: [textElement],
|
||||
props: {
|
||||
element: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
paragraphs() {
|
||||
return this.value
|
||||
return this.element.value
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<form @submit.prevent="submit">
|
||||
<form @submit.prevent>
|
||||
<FormElement class="control">
|
||||
<label class="control__label">
|
||||
{{ $t('headingElementForm.levelTitle') }}
|
||||
|
|
|
@ -0,0 +1,256 @@
|
|||
<template>
|
||||
<form @submit.prevent>
|
||||
<FormElement class="control">
|
||||
<label class="control__label">
|
||||
{{ $t('linkElementForm.text') }}
|
||||
</label>
|
||||
<div class="control__elements">
|
||||
<input
|
||||
v-model="values.value"
|
||||
type="text"
|
||||
class="input"
|
||||
:placeholder="$t('linkElementForm.textPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
</FormElement>
|
||||
<FormElement class="control">
|
||||
<label class="control__label">
|
||||
{{ $t('linkElementForm.navigateTo') }}
|
||||
</label>
|
||||
<div class="control__elements">
|
||||
<Dropdown v-model="navigateTo" :show-search="false">
|
||||
<template #value>
|
||||
<template v-if="destinationPage">
|
||||
{{ destinationPage.name }}
|
||||
<span class="link-element-form__navigate-option-page-path">
|
||||
{{ destinationPage.path }}
|
||||
</span></template
|
||||
>
|
||||
<span v-else>{{ $t('linkElementForm.navigateToCustom') }}</span>
|
||||
</template>
|
||||
<DropdownItem
|
||||
v-for="page in pages"
|
||||
:key="page.id"
|
||||
:value="page.id"
|
||||
:name="page.name"
|
||||
>
|
||||
{{ page.name }}
|
||||
<span class="link-element-form__navigate-option-page-path">
|
||||
{{ page.path }}
|
||||
</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
:name="$t('linkElementForm.navigateToCustom')"
|
||||
value="custom"
|
||||
></DropdownItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</FormElement>
|
||||
<FormElement v-if="navigateTo === 'custom'" class="control">
|
||||
<label class="control__label">
|
||||
{{ $t('linkElementForm.url') }}
|
||||
</label>
|
||||
<div class="control__elements">
|
||||
<input
|
||||
v-model="values.navigate_to_url"
|
||||
type="text"
|
||||
class="input"
|
||||
:placeholder="$t('linkElementForm.urlPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
</FormElement>
|
||||
<FormElement
|
||||
v-if="destinationPage"
|
||||
class="control link-element-form__params"
|
||||
>
|
||||
<template v-if="parametersInError">
|
||||
<Alert type="error" minimal>
|
||||
<div class="link-element-form__params-error">
|
||||
<div>
|
||||
{{ $t('linkElementForm.paramsInErrorDescription') }}
|
||||
</div>
|
||||
<Button
|
||||
color="error"
|
||||
size="tiny"
|
||||
@click.prevent="updatePageParameters"
|
||||
>
|
||||
{{ $t('linkElementForm.paramsInErrorButton') }}
|
||||
</Button>
|
||||
</div>
|
||||
</Alert>
|
||||
</template>
|
||||
<div v-else class="control__elements link-element-form__params">
|
||||
<template v-for="param in values.page_parameters">
|
||||
<!-- eslint-disable-next-line vue/require-v-for-key -->
|
||||
<label>{{ param.name }}</label>
|
||||
<!-- eslint-disable-next-line vue/require-v-for-key -->
|
||||
<input
|
||||
v-model="param.value"
|
||||
type="text"
|
||||
class="input"
|
||||
:placeholder="$t('linkElementForm.paramPlaceholder')"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</FormElement>
|
||||
<FormElement class="control">
|
||||
<label class="control__label">
|
||||
{{ $t('linkElementForm.variant') }}
|
||||
</label>
|
||||
<div class="control__elements">
|
||||
<RadioButton v-model="values.variant" value="link">
|
||||
{{ $t('linkElementForm.variantLink') }}
|
||||
</RadioButton>
|
||||
<RadioButton v-model="values.variant" value="button">
|
||||
{{ $t('linkElementForm.variantButton') }}
|
||||
</RadioButton>
|
||||
</div>
|
||||
</FormElement>
|
||||
<FormElement class="control">
|
||||
<label class="control__label">
|
||||
{{ $t('linkElementForm.alignment') }}
|
||||
</label>
|
||||
<div class="control__elements">
|
||||
<RadioButton
|
||||
v-for="alignment in alignments"
|
||||
:key="alignment.value"
|
||||
v-model="values.alignment"
|
||||
:value="alignment.value"
|
||||
:icon="alignment.icon"
|
||||
:title="alignment.name"
|
||||
/>
|
||||
</div>
|
||||
</FormElement>
|
||||
<FormElement class="control">
|
||||
<label class="control__label">
|
||||
{{ $t('linkElementForm.target') }}
|
||||
</label>
|
||||
<div class="control__elements">
|
||||
<RadioButton v-model="values.target" value="self">
|
||||
{{ $t('linkElementForm.targetSelf') }}
|
||||
</RadioButton>
|
||||
<RadioButton v-model="values.target" value="blank">
|
||||
{{ $t('linkElementForm.targetNewTab') }}
|
||||
</RadioButton>
|
||||
</div>
|
||||
</FormElement>
|
||||
<FormElement v-if="values.variant === 'button'" class="control">
|
||||
<label class="control__label">
|
||||
{{ $t('linkElementForm.width') }}
|
||||
</label>
|
||||
<div class="control__elements">
|
||||
<RadioButton v-model="values.width" value="auto">
|
||||
{{ $t('linkElementForm.widthAuto') }}
|
||||
</RadioButton>
|
||||
<RadioButton v-model="values.width" value="full">
|
||||
{{ $t('linkElementForm.widthFull') }}
|
||||
</RadioButton>
|
||||
</div>
|
||||
</FormElement>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import form from '@baserow/modules/core/mixins/form'
|
||||
import { LinkElementType } from '@baserow/modules/builder/elementTypes'
|
||||
|
||||
export default {
|
||||
name: 'LinkElementForm',
|
||||
mixins: [form],
|
||||
props: { builder: { type: Object, required: true } },
|
||||
data() {
|
||||
let navigateTo = ''
|
||||
if (this.defaultValues.navigation_type === 'page') {
|
||||
if (this.defaultValues.navigate_to_page_id) {
|
||||
navigateTo = this.defaultValues.navigate_to_page_id
|
||||
}
|
||||
} else {
|
||||
navigateTo = 'custom'
|
||||
}
|
||||
return {
|
||||
values: {
|
||||
value: '',
|
||||
alignment: 'left',
|
||||
variant: 'link',
|
||||
navigation_type: 'page',
|
||||
navigate_to_page_id: null,
|
||||
navigate_to_url: '',
|
||||
page_parameters: [],
|
||||
width: 'auto',
|
||||
target: 'self',
|
||||
},
|
||||
parametersInError: false,
|
||||
navigateTo,
|
||||
alignments: [
|
||||
{
|
||||
name: this.$t('linkElementForm.alignmentLeft'),
|
||||
value: 'left',
|
||||
icon: 'align-left',
|
||||
},
|
||||
{
|
||||
name: this.$t('linkElementForm.alignmentCenter'),
|
||||
value: 'center',
|
||||
icon: 'align-center',
|
||||
},
|
||||
{
|
||||
name: this.$t('linkElementForm.alignmentRight'),
|
||||
value: 'right',
|
||||
icon: 'align-right',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pages() {
|
||||
return this.builder.pages
|
||||
},
|
||||
destinationPage() {
|
||||
if (!isNaN(this.navigateTo)) {
|
||||
return this.builder.pages.find(({ id }) => id === this.navigateTo)
|
||||
}
|
||||
return null
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'destinationPage.path_params': {
|
||||
handler(value) {
|
||||
this.updatePageParameters()
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
navigateTo(value) {
|
||||
if (value === '') {
|
||||
this.values.navigation_type = 'page'
|
||||
this.values.navigate_to_page_id = null
|
||||
this.values.navigate_to_url = ''
|
||||
} else if (value === 'custom') {
|
||||
this.values.navigation_type = 'custom'
|
||||
} else if (!isNaN(value)) {
|
||||
this.values.navigation_type = 'page'
|
||||
this.values.navigate_to_page_id = value
|
||||
this.updatePageParameters()
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (LinkElementType.arePathParametersInError(this.values, this.builder)) {
|
||||
this.parametersInError = true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updatePageParameters() {
|
||||
this.values.page_parameters = (
|
||||
this.destinationPage?.path_params || []
|
||||
).map(({ name }, index) => {
|
||||
let value = ''
|
||||
// Naive way to keep data when we change the destination page.
|
||||
if (this.values.page_parameters[index]) {
|
||||
value = this.values.page_parameters[index].value
|
||||
}
|
||||
return { name, value }
|
||||
})
|
||||
this.parametersInError = false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<form @submit.prevent="submit">
|
||||
<form @submit.prevent>
|
||||
<FormElement class="control">
|
||||
<label class="control__label">
|
||||
{{ $t('paragraphElementForm.textTitle') }}
|
||||
|
|
|
@ -37,11 +37,8 @@
|
|||
<script>
|
||||
export default {
|
||||
name: 'PageActions',
|
||||
inject: ['builder'],
|
||||
props: {
|
||||
builder: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
page: {
|
||||
type: Object,
|
||||
required: true,
|
||||
|
|
|
@ -4,14 +4,17 @@
|
|||
:is="getType(element).component"
|
||||
v-for="element in elements"
|
||||
:key="element.id"
|
||||
v-bind="getType(element).getComponentProps(element)"
|
||||
:element="element"
|
||||
class="element__component"
|
||||
:builder="builder"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
inject: ['builder', 'mode'],
|
||||
props: {
|
||||
page: {
|
||||
type: Object,
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<header class="layout__col-2-1 header header--space-between">
|
||||
<PageHeaderMenuItems :page="page" :builder="builder" />
|
||||
<PageHeaderMenuItems :page="page" />
|
||||
<DeviceSelector
|
||||
:device-type-selected="deviceTypeSelected"
|
||||
@selected="actionSetDeviceTypeSelected"
|
||||
/>
|
||||
<PageActions :builder="builder" :page="page" />
|
||||
<PageActions :page="page" />
|
||||
</header>
|
||||
</template>
|
||||
|
||||
|
@ -23,10 +23,6 @@ export default {
|
|||
PageActions,
|
||||
},
|
||||
props: {
|
||||
builder: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
page: {
|
||||
type: Object,
|
||||
required: true,
|
||||
|
|
|
@ -13,12 +13,7 @@
|
|||
<i class="header__filter-icon fas" :class="`fa-${itemType.icon}`"></i>
|
||||
<span class="header__filter-name">{{ itemType.label }}</span>
|
||||
</a>
|
||||
<component
|
||||
:is="itemType.component"
|
||||
ref="component"
|
||||
:builder="builder"
|
||||
:page="page"
|
||||
/>
|
||||
<component :is="itemType.component" ref="component" :page="page" />
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
@ -31,10 +26,6 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
builder: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
pageHeaderItemTypes() {
|
||||
|
|
|
@ -37,11 +37,8 @@ export default {
|
|||
name: 'PageSettings',
|
||||
components: { PageSettingsForm },
|
||||
mixins: [error],
|
||||
inject: ['builder'],
|
||||
props: {
|
||||
builder: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
page: {
|
||||
type: Object,
|
||||
required: true,
|
||||
|
|
|
@ -23,11 +23,7 @@
|
|||
</ul>
|
||||
</template>
|
||||
<template v-if="settingSelected" #content>
|
||||
<component
|
||||
:is="settingSelected.component"
|
||||
:builder="builder"
|
||||
:page="page"
|
||||
></component>
|
||||
<component :is="settingSelected.component" :page="page"></component>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
@ -39,10 +35,6 @@ export default {
|
|||
name: 'PageSettingsModal',
|
||||
mixins: [modal],
|
||||
props: {
|
||||
builder: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
page: {
|
||||
type: Object,
|
||||
required: true,
|
||||
|
|
|
@ -10,15 +10,11 @@
|
|||
|
||||
<script>
|
||||
import PageContent from '@baserow/modules/builder/components/page/PageContent'
|
||||
import PublicBuilderService from '@baserow/modules/builder/services/publicBuilder.js'
|
||||
import PublicBuilderService from '@baserow/modules/builder/services/publishedDomain.js'
|
||||
|
||||
export default {
|
||||
components: { PageContent },
|
||||
props: {
|
||||
builder: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
page: {
|
||||
type: Object,
|
||||
required: true,
|
||||
|
|
|
@ -85,7 +85,7 @@ import { mapActions, mapGetters } from 'vuex'
|
|||
import modal from '@baserow/modules/core/mixins/modal'
|
||||
import error from '@baserow/modules/core/mixins/error'
|
||||
import jobProgress from '@baserow/modules/core/mixins/jobProgress'
|
||||
import BuilderService from '@baserow/modules/builder/services/publicBuilder.js'
|
||||
import PublishedDomainService from '@baserow/modules/builder/services/publishedDomain.js'
|
||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||
import { copyToClipboard } from '@baserow/modules/database/utils/clipboard'
|
||||
import LastPublishedDomainDate from '@baserow/modules/builder/components/domain/LastPublishedDomainDate'
|
||||
|
@ -134,7 +134,7 @@ export default {
|
|||
async publishSite() {
|
||||
this.loading = true
|
||||
this.hideError()
|
||||
const { data: job } = await BuilderService(this.$client).publish({
|
||||
const { data: job } = await PublishedDomainService(this.$client).publish({
|
||||
id: this.selectedDomain,
|
||||
})
|
||||
|
||||
|
|
|
@ -3,8 +3,9 @@
|
|||
:is="elementType.formComponent"
|
||||
:key="element.id"
|
||||
ref="elementForm"
|
||||
:default-values="defaultValues"
|
||||
class="element-form"
|
||||
:builder="builder"
|
||||
:default-values="defaultValues"
|
||||
@values-changed="onChange($event)"
|
||||
/>
|
||||
</template>
|
||||
|
@ -12,13 +13,12 @@
|
|||
<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'
|
||||
|
||||
export default {
|
||||
name: 'GeneralSidePanel',
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
inject: ['builder'],
|
||||
computed: {
|
||||
...mapGetters({
|
||||
element: 'element/getSelected',
|
||||
|
@ -32,7 +32,7 @@ export default {
|
|||
},
|
||||
|
||||
defaultValues() {
|
||||
return this.elementType.getComponentProps(this.element)
|
||||
return this.element
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
@ -40,11 +40,13 @@ export default {
|
|||
actionDebouncedUpdateSelectedElement: 'element/debouncedUpdateSelected',
|
||||
}),
|
||||
async onChange(newValues) {
|
||||
const oldValues = this.elementType.getComponentProps(this.element)
|
||||
const oldValues = this.element
|
||||
if (!_.isEqual(newValues, oldValues)) {
|
||||
try {
|
||||
await this.actionDebouncedUpdateSelectedElement({
|
||||
values: newValues,
|
||||
// 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
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
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'
|
||||
|
||||
import _ from 'lodash'
|
||||
|
||||
import { compile } from 'path-to-regexp'
|
||||
|
||||
export class ElementType extends Registerable {
|
||||
get name() {
|
||||
|
@ -21,30 +28,21 @@ export class ElementType extends Registerable {
|
|||
return null
|
||||
}
|
||||
|
||||
get editComponent() {
|
||||
return this.component
|
||||
}
|
||||
|
||||
get formComponent() {
|
||||
return null
|
||||
}
|
||||
|
||||
get properties() {
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the attributes of the element instance into attributes that the component
|
||||
* can use. The returned object needs to be a mapping from the name of the property
|
||||
* at the component level to the value in the element object.
|
||||
*
|
||||
* Example:
|
||||
* - Let's say you have a prop called `level`
|
||||
* - The element looks like this: { 'id': 'someId', 'level': 1 }
|
||||
*
|
||||
* Then you will have to return { 'level': element.level }
|
||||
*
|
||||
* @param element
|
||||
* @returns {{}}
|
||||
* Returns whether the element configuration is valid or not.
|
||||
* @param {object} param An object containing the element and the builder
|
||||
* @returns true if the element is in error
|
||||
*/
|
||||
getComponentProps(element) {
|
||||
return {}
|
||||
isInError({ element, builder }) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,13 +70,6 @@ export class HeadingElementType extends ElementType {
|
|||
get formComponent() {
|
||||
return HeadingElementForm
|
||||
}
|
||||
|
||||
getComponentProps(element) {
|
||||
return {
|
||||
value: element.value,
|
||||
level: element.level,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ParagraphElementType extends ElementType {
|
||||
|
@ -105,10 +96,91 @@ export class ParagraphElementType extends ElementType {
|
|||
get formComponent() {
|
||||
return ParagraphElementForm
|
||||
}
|
||||
}
|
||||
|
||||
getComponentProps(element) {
|
||||
return {
|
||||
value: element.value,
|
||||
export class LinkElementType extends ElementType {
|
||||
getType() {
|
||||
return 'link'
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.app.i18n.t('elementType.link')
|
||||
}
|
||||
|
||||
get description() {
|
||||
return this.app.i18n.t('elementType.linkDescription')
|
||||
}
|
||||
|
||||
get iconClass() {
|
||||
return 'link'
|
||||
}
|
||||
|
||||
get component() {
|
||||
return LinkElement
|
||||
}
|
||||
|
||||
get editComponent() {
|
||||
return LinkElementEdit
|
||||
}
|
||||
|
||||
get formComponent() {
|
||||
return LinkElementForm
|
||||
}
|
||||
|
||||
isInError({ element, builder }) {
|
||||
try {
|
||||
LinkElementType.getUrlFromElement(element, builder)
|
||||
} catch (e) {
|
||||
// Error in path resolution
|
||||
return true
|
||||
}
|
||||
|
||||
return LinkElementType.arePathParametersInError(element, builder)
|
||||
}
|
||||
|
||||
static arePathParametersInError(element, builder) {
|
||||
if (
|
||||
element.navigation_type === 'page' &&
|
||||
!isNaN(element.navigate_to_page_id)
|
||||
) {
|
||||
const destinationPageParamNames = (
|
||||
builder.pages.find(({ id }) => id === element.navigate_to_page_id)
|
||||
?.path_params || []
|
||||
).map(({ name }) => name)
|
||||
|
||||
const pageParams = element.page_parameters.map(({ name }) => name)
|
||||
|
||||
if (!_.isEqual(destinationPageParamNames, pageParams)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
static getUrlFromElement(element, builder) {
|
||||
if (element.navigation_type === 'page') {
|
||||
if (!isNaN(element.navigate_to_page_id)) {
|
||||
const page = builder.pages.find(
|
||||
({ id }) => id === element.navigate_to_page_id
|
||||
)
|
||||
|
||||
// The builder page list might be empty or the page has been deleted
|
||||
if (!page) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const toPath = compile(page.path, { encode: encodeURIComponent })
|
||||
const pageParams = Object.fromEntries(
|
||||
element.page_parameters.map(({ name, value }) => [name, value])
|
||||
)
|
||||
return toPath(pageParams)
|
||||
}
|
||||
} else if (!element.navigate_to_url.startsWith('http')) {
|
||||
// add the https protocol if missing
|
||||
return `https://${element.navigate_to_url}`
|
||||
} else {
|
||||
return element.navigate_to_url
|
||||
}
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,7 +57,9 @@
|
|||
"heading": "Heading",
|
||||
"headingDescription": "Page heading title",
|
||||
"paragraph": "Paragraph",
|
||||
"paragraphDescription": "Single line text"
|
||||
"paragraphDescription": "Single line text",
|
||||
"link": "Link",
|
||||
"linkDescription": "A link to page/URL"
|
||||
},
|
||||
"addElementButton": {
|
||||
"label": "Element"
|
||||
|
@ -125,6 +127,34 @@
|
|||
"domainTypes": {
|
||||
"customName": "Custom domain"
|
||||
},
|
||||
"linkElement": {
|
||||
"noValue": "Unnamed..."
|
||||
},
|
||||
"linkElementForm": {
|
||||
"text": "Text",
|
||||
"textPlaceholder": "Enter text...",
|
||||
"navigateTo": "Navigate to",
|
||||
"navigateToNotSet": "No destination",
|
||||
"navigateToCustom": "Custom URL",
|
||||
"url": "Destination URL",
|
||||
"urlPlaceholder": "Enter an URL...",
|
||||
"variant": "Variant",
|
||||
"variantLink": "Link",
|
||||
"variantButton": "Button",
|
||||
"alignment": "Alignment",
|
||||
"alignmentLeft": "Link",
|
||||
"alignmentCenter": "Center",
|
||||
"alignmentRight": "Right",
|
||||
"width": "Width",
|
||||
"widthAuto": "Auto",
|
||||
"widthFull": "Full width",
|
||||
"target": "Open in...",
|
||||
"targetSelf": "Same tab",
|
||||
"targetNewTab": "New tab",
|
||||
"paramPlaceholder": "Enter a value...",
|
||||
"paramsInErrorDescription": "The saved parameters don't match the page parameters. The page has probably been deleted or updated.",
|
||||
"paramsInErrorButton": "Update parameters"
|
||||
},
|
||||
"pageSettingsTypes": {
|
||||
"pageName": "Page"
|
||||
},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="page-editor">
|
||||
<PageHeader :builder="builder" :page="page" />
|
||||
<PageHeader :page="page" />
|
||||
<div class="layout__col-2-2 page-editor__content">
|
||||
<div :style="{ width: `calc(100% - ${panelWidth}px)` }">
|
||||
<PagePreview />
|
||||
|
@ -21,6 +21,9 @@ import PageSidePanels from '@baserow/modules/builder/components/page/PageSidePan
|
|||
export default {
|
||||
name: 'PageEditor',
|
||||
components: { PagePreview, PageHeader, PageSidePanels },
|
||||
provide() {
|
||||
return { builder: this.builder }
|
||||
},
|
||||
/**
|
||||
* When the user leaves to another page we want to unselect the selected page. This
|
||||
* way it will not be highlighted the left sidebar.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<PublicPage :builder="builder" :page="page" :path="path" :params="params" />
|
||||
<PublicPage :page="page" :path="path" :params="params" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -8,13 +8,16 @@ import { resolveApplicationRoute } from '@baserow/modules/builder/utils/routing'
|
|||
|
||||
export default {
|
||||
components: { PublicPage },
|
||||
|
||||
provide() {
|
||||
return { builder: this.builder, mode: this.mode }
|
||||
},
|
||||
async asyncData(context) {
|
||||
let builder = context.store.getters['publicBuilder/getBuilder']
|
||||
let mode = 'public'
|
||||
const builderId = context.route.params.builderId
|
||||
|
||||
if (!builder) {
|
||||
try {
|
||||
const builderId = context.route.params.builderId
|
||||
if (builderId) {
|
||||
// We have the builderId in the params so this is a preview
|
||||
// Must fetch the builder instance by this Id.
|
||||
|
@ -42,6 +45,10 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
if (builderId) {
|
||||
mode = 'preview'
|
||||
}
|
||||
|
||||
const found = resolveApplicationRoute(
|
||||
builder.pages,
|
||||
context.route.params.pathMatch
|
||||
|
@ -62,6 +69,7 @@ export default {
|
|||
page,
|
||||
path,
|
||||
params,
|
||||
mode,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import { registerRealtimeEvents } from '@baserow/modules/builder/realtime'
|
|||
import {
|
||||
HeadingElementType,
|
||||
ParagraphElementType,
|
||||
LinkElementType,
|
||||
} from '@baserow/modules/builder/elementTypes'
|
||||
import {
|
||||
DesktopDeviceType,
|
||||
|
@ -103,6 +104,7 @@ export default (context) => {
|
|||
|
||||
app.$registry.register('element', new HeadingElementType(context))
|
||||
app.$registry.register('element', new ParagraphElementType(context))
|
||||
app.$registry.register('element', new LinkElementType(context))
|
||||
|
||||
app.$registry.register('device', new DesktopDeviceType(context))
|
||||
app.$registry.register('device', new TabletDeviceType(context))
|
||||
|
|
|
@ -56,7 +56,10 @@ const actions = {
|
|||
forceUpdate({ commit }, { element, values }) {
|
||||
commit('UPDATE_ITEM', { element, values })
|
||||
},
|
||||
forceDelete({ commit }, { elementId }) {
|
||||
forceDelete({ commit, getters }, { elementId }) {
|
||||
if (getters.getSelected.id === elementId) {
|
||||
commit('SELECT_ITEM', { element: null })
|
||||
}
|
||||
commit('DELETE_ITEM', { elementId })
|
||||
},
|
||||
forceMove({ commit, getters }, { elementId, beforeElementId }) {
|
||||
|
|
|
@ -89,6 +89,15 @@ const actions = {
|
|||
|
||||
commit('SET_SELECTED', { builder, page })
|
||||
|
||||
// Unselect previously selected element
|
||||
dispatch(
|
||||
'element/select',
|
||||
{
|
||||
element: null,
|
||||
},
|
||||
{ root: true }
|
||||
)
|
||||
|
||||
return { builder, page }
|
||||
},
|
||||
unselect({ commit }) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import PublicBuilderService from '@baserow/modules/builder/services/publicBuilder'
|
||||
import PublishedDomainService from '@baserow/modules/builder/services/publishedDomain'
|
||||
|
||||
const state = {
|
||||
// The public builder loaded
|
||||
|
@ -18,7 +18,7 @@ const actions = {
|
|||
async fetchById({ commit }, { builderId }) {
|
||||
commit('CLEAR_ITEM')
|
||||
|
||||
const { data } = await PublicBuilderService(this.$client).fetchById(
|
||||
const { data } = await PublishedDomainService(this.$client).fetchById(
|
||||
builderId
|
||||
)
|
||||
commit('SET_ITEM', { builder: data })
|
||||
|
@ -27,7 +27,7 @@ const actions = {
|
|||
async fetchByDomain({ commit }, { domain }) {
|
||||
commit('CLEAR_ITEM')
|
||||
|
||||
const { data } = await PublicBuilderService(this.$client).fetchByDomain(
|
||||
const { data } = await PublishedDomainService(this.$client).fetchByDomain(
|
||||
domain
|
||||
)
|
||||
commit('SET_ITEM', { builder: data })
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { pathToRegexp } from 'path-to-regexp'
|
||||
import pathToRegexp from 'path-to-regexp'
|
||||
|
||||
export const resolveApplicationRoute = (pages, fullPath) => {
|
||||
let found
|
||||
|
|
|
@ -9,7 +9,9 @@
|
|||
@import "page";
|
||||
@import "elements/headingElement";
|
||||
@import "elements/paragraphElement";
|
||||
@import "elements/linkElement";
|
||||
@import "elements/forms/paragraphElementForm";
|
||||
@import "elements/forms/linkElementForm";
|
||||
@import "page_settings_path_params_form_element";
|
||||
@import "domain_card";
|
||||
@import "dns_status";
|
||||
|
|
|
@ -138,3 +138,20 @@
|
|||
.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;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
.link-element-form__params {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
grid-gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.link-element-form__navigate-option-page-path {
|
||||
font-size: 12px;
|
||||
color: $color-neutral-500;
|
||||
margin-left: 5px;
|
||||
}
|
|
@ -2,3 +2,11 @@
|
|||
resize: vertical;
|
||||
color: $color-primary-900;
|
||||
}
|
||||
|
||||
.link-element-form__params-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
.link-element {
|
||||
display: flex;
|
||||
padding: 5px 82px;
|
||||
}
|
||||
|
||||
.link-element--alignment-left {
|
||||
justify-content: start;
|
||||
|
||||
.button--full-width {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.link-element--alignment-center {
|
||||
justify-content: center;
|
||||
|
||||
.button--full-width {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.link-element--alignment-right {
|
||||
justify-content: end;
|
||||
|
||||
.button--full-width {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.link-element__link {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
padding: 0;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
border: 1px solid transparent;
|
||||
}
|
|
@ -60,6 +60,27 @@
|
|||
border-color: $color-neutral-400;
|
||||
}
|
||||
|
||||
.button--light {
|
||||
@include button-style(transparent, $color-neutral-100, $color-primary-900);
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active,
|
||||
&.button-loading {
|
||||
text-decoration: none;
|
||||
box-shadow: none;
|
||||
background-color: $color-neutral-100;
|
||||
}
|
||||
|
||||
&.button--loading::after {
|
||||
border-color: $color-neutral-900 transparent $color-neutral-900 transparent;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $color-primary-100;
|
||||
}
|
||||
}
|
||||
|
||||
.button__icon {
|
||||
margin: 0 2px;
|
||||
|
||||
|
@ -104,7 +125,6 @@
|
|||
.button--full-width {
|
||||
@include button-size(15px, 44px, 28px);
|
||||
|
||||
margin-bottom: 16px;
|
||||
width: 100%;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@
|
|||
}
|
||||
|
||||
&.alert--minimal {
|
||||
max-width: 228px;
|
||||
white-space: normal;
|
||||
margin: 16px 0;
|
||||
padding: 14px;
|
||||
|
|
124
web-frontend/modules/core/components/Button.vue
Normal file
124
web-frontend/modules/core/components/Button.vue
Normal file
|
@ -0,0 +1,124 @@
|
|||
<template>
|
||||
<component
|
||||
:is="type === 'link' || href ? 'a' : 'button'"
|
||||
class="button"
|
||||
:class="classes"
|
||||
:disable="disabled"
|
||||
:active="active"
|
||||
v-bind.prop="customBind"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<i
|
||||
v-if="prependIcon"
|
||||
class="button__icon fas"
|
||||
:class="`fa-${prependIcon}`"
|
||||
/>
|
||||
<slot />
|
||||
<i
|
||||
v-if="appendIcon || icon"
|
||||
class="button__icon fas"
|
||||
:class="appendIcon ? `fa-${appendIcon}` : `fa-${icon}`"
|
||||
/>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
type: {
|
||||
// link - button
|
||||
required: false,
|
||||
type: String,
|
||||
default: 'button',
|
||||
},
|
||||
size: {
|
||||
// tiny - normal - large
|
||||
required: false,
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
color: {
|
||||
// primary - success - warning - error - ghost - light
|
||||
required: false,
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
prependIcon: {
|
||||
required: false,
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
appendIcon: {
|
||||
required: false,
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
icon: {
|
||||
required: false,
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
loading: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
fullWidth: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
active: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
overflow: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
href: {
|
||||
required: false,
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
target: {
|
||||
required: false,
|
||||
type: String,
|
||||
default: 'self',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
classes() {
|
||||
const classObj = {
|
||||
[`button--${this.size}`]: this.size,
|
||||
[`button--${this.color}`]: this.color,
|
||||
'button--primary': !this.color,
|
||||
'button--full-width': this.fullWidth,
|
||||
'button--icon': this.prependIcon || this.appendIcon || this.icon,
|
||||
'button--loading': this.loading,
|
||||
disabled: this.disabled,
|
||||
active: this.active,
|
||||
'button--overflow': this.overflow,
|
||||
}
|
||||
return classObj
|
||||
},
|
||||
customBind() {
|
||||
const attr = {}
|
||||
if (this.href) {
|
||||
attr.href = this.href
|
||||
}
|
||||
if (this.target) {
|
||||
attr.target = `_${this.target}`
|
||||
}
|
||||
return attr
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
47
web-frontend/modules/core/components/RadioButton.vue
Normal file
47
web-frontend/modules/core/components/RadioButton.vue
Normal file
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<Button color="light" v-bind="restProps" @click.prevent="select(value)">
|
||||
<slot></slot>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'RadioButton',
|
||||
model: {
|
||||
prop: 'modelValue',
|
||||
event: 'input',
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Number, Boolean, Object],
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
modelValue: {
|
||||
type: [String, Number, Boolean, Object],
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
selected() {
|
||||
return this.modelValue === this.value
|
||||
},
|
||||
restProps() {
|
||||
const { value, modelValue, ...rest } = this.$attrs
|
||||
if (this.selected) {
|
||||
rest.active = true
|
||||
}
|
||||
return rest
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
select(value) {
|
||||
if (this.disabled || this.selected) {
|
||||
return
|
||||
}
|
||||
this.$emit('input', value)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -23,7 +23,7 @@
|
|||
:href="getHref(index)"
|
||||
class="tabs__link"
|
||||
:class="{ 'tabs__link--disabled': tab.disabled }"
|
||||
@click.prevent=""
|
||||
@click.prevent
|
||||
>
|
||||
{{ tab.title }}
|
||||
</a>
|
||||
|
|
|
@ -45,7 +45,7 @@ export default {
|
|||
*/
|
||||
getDefaultValues() {
|
||||
if (this.allowedValues === null) {
|
||||
return this.defaultValues
|
||||
return clone(this.defaultValues)
|
||||
}
|
||||
return Object.keys(this.defaultValues).reduce((result, key) => {
|
||||
if (this.allowedValues.includes(key)) {
|
||||
|
|
|
@ -144,6 +144,77 @@
|
|||
<Radio v-model="radio" value="e" :loading="true">Option E</Radio>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<label class="control__label">Radio buttons</label>
|
||||
<div
|
||||
class="control__elements"
|
||||
:style="{ backgroundColor: 'white', padding: '5px' }"
|
||||
>
|
||||
value: {{ radioButton }}
|
||||
<br />
|
||||
<br />
|
||||
<RadioButton v-model="radioButton" value="">None</RadioButton>
|
||||
<RadioButton v-model="radioButton" value="a"
|
||||
>Option A</RadioButton
|
||||
>
|
||||
<RadioButton v-model="radioButton" value="b"
|
||||
>Option B</RadioButton
|
||||
>
|
||||
<RadioButton v-model="radioButton" value="c"
|
||||
>Option C</RadioButton
|
||||
>
|
||||
|
||||
<RadioButton v-model="radioButton" value="g" :disabled="true">
|
||||
Option D
|
||||
</RadioButton>
|
||||
<RadioButton v-model="radioButton" value="h" :loading="true">
|
||||
Option E
|
||||
</RadioButton>
|
||||
</div>
|
||||
<div
|
||||
class="control__elements"
|
||||
:style="{ backgroundColor: 'white', padding: '5px' }"
|
||||
>
|
||||
<RadioButton
|
||||
v-model="radioButton"
|
||||
value="d"
|
||||
icon="align-left"
|
||||
></RadioButton>
|
||||
<RadioButton
|
||||
v-model="radioButton"
|
||||
value="e"
|
||||
icon="align-center"
|
||||
></RadioButton>
|
||||
<RadioButton
|
||||
v-model="radioButton"
|
||||
value="f"
|
||||
icon="align-right"
|
||||
></RadioButton>
|
||||
</div>
|
||||
<div
|
||||
class="control__elements"
|
||||
:style="{ backgroundColor: 'white', padding: '5px' }"
|
||||
>
|
||||
<RadioButton
|
||||
v-model="radioButton"
|
||||
value="d"
|
||||
icon="align-left"
|
||||
size="large"
|
||||
></RadioButton>
|
||||
<RadioButton
|
||||
v-model="radioButton"
|
||||
value="e"
|
||||
icon="align-center"
|
||||
size="large"
|
||||
></RadioButton>
|
||||
<RadioButton
|
||||
v-model="radioButton"
|
||||
value="f"
|
||||
icon="align-right"
|
||||
size="large"
|
||||
></RadioButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<label class="control__label">Switch field</label>
|
||||
<div class="control__elements">
|
||||
|
@ -408,100 +479,86 @@
|
|||
urna. Praesent.
|
||||
</Alert>
|
||||
</div>
|
||||
|
||||
<div class="margin-bottom-3 style-guide__buttons">
|
||||
<a class="button">a.button</a>
|
||||
<a class="button disabled">a.button[disabled]</a>
|
||||
<a class="button button--success">a.button.button-success</a>
|
||||
<a class="button button--warning">a.button.button-warning</a>
|
||||
<a class="button button--error">a.button.button-error</a>
|
||||
<a class="button">
|
||||
a.button
|
||||
<i class="button__icon fas fa-lock-open"></i>
|
||||
</a>
|
||||
<a class="button">
|
||||
<i class="button__icon fas fa-arrow-left"></i>
|
||||
a.button
|
||||
</a>
|
||||
<a class="button button--ghost">a.button.button-ghosts</a>
|
||||
<a class="button">
|
||||
<i class="fas fa-user-check"></i>
|
||||
</a>
|
||||
<a class="button button--ghost">
|
||||
<i class="fas fa-user-check"></i>
|
||||
</a>
|
||||
<h3>Tiny size</h3>
|
||||
<Button size="tiny">button</Button>
|
||||
<Button size="tiny" disabled>button[disabled]</Button>
|
||||
<Button size="tiny" color="primary">button.button-success</Button>
|
||||
<Button size="tiny" color="success">button.button-success</Button>
|
||||
<Button size="tiny" color="warning">button.button-warning</Button>
|
||||
<Button size="tiny" color="error">button.button-error</Button>
|
||||
<Button size="tiny" append-icon="lock-open">button</Button>
|
||||
<Button size="tiny" prepend-icon="arrow-left">button</Button>
|
||||
<Button size="tiny" color="ghost">button.button-ghost</Button>
|
||||
<Button size="tiny" icon="user-check"></Button>
|
||||
<Button size="tiny" icon="user-check" color="ghost"></Button>
|
||||
</div>
|
||||
<div class="margin-bottom-3 style-guide__buttons">
|
||||
<button class="button">button.button</button>
|
||||
<button class="button" disabled>button.button[disabled]</button>
|
||||
<button class="button button--success">
|
||||
button.button.button-success
|
||||
</button>
|
||||
<button class="button button--warning">
|
||||
button.button.button-warning
|
||||
</button>
|
||||
<button class="button button--error">
|
||||
button.button.button-error
|
||||
</button>
|
||||
<button class="button">
|
||||
button.button
|
||||
<i class="button__icon fas fa-lock-open"></i>
|
||||
</button>
|
||||
<button class="button">
|
||||
<i class="button__icon fas fa-arrow-left"></i>
|
||||
button.button
|
||||
</button>
|
||||
<button class="button button--ghost">
|
||||
button.button.ghost-button
|
||||
</button>
|
||||
<button class="button">
|
||||
<i class="fas fa-user-check"></i>
|
||||
</button>
|
||||
<button class="button button--ghost">
|
||||
<i class="fas fa-user-check"></i>
|
||||
</button>
|
||||
<h3>Normal link</h3>
|
||||
<Button type="link" href="#">a.button</Button>
|
||||
<Button type="link" disabled href="#">a.button[disabled]</Button>
|
||||
<Button type="link" color="success" href="#">
|
||||
a.button.button-success
|
||||
</Button>
|
||||
<Button type="link" color="warning">a.button.button-warning</Button>
|
||||
<Button type="link" color="error">a.button.button-error</Button>
|
||||
<Button type="link" append-icon="lock-open">a.button</Button>
|
||||
<Button type="link" prepend-icon="arrow-left">a.button</Button>
|
||||
<Button type="link" color="ghost">a.button.button-ghost</Button>
|
||||
<Button type="link" icon="user-check"></Button>
|
||||
<Button type="link" icon="user-check" color="ghost"></Button>
|
||||
</div>
|
||||
<div
|
||||
class="margin-bottom-3 style-guide__buttons"
|
||||
:style="{ backgroundColor: 'white', padding: '5px' }"
|
||||
>
|
||||
<h3>Normal button</h3>
|
||||
<Button>button.button</Button>
|
||||
<Button disabled>button[disabled]</Button>
|
||||
<Button color="success">button.button-success</Button>
|
||||
<Button color="warning">button.button-warning</Button>
|
||||
<Button color="error">button.button-error</Button>
|
||||
<Button append-icon="lock-open">button</Button>
|
||||
<Button prepend-icon="arrow-left">button</Button>
|
||||
<Button color="ghost">button.button-ghost</Button>
|
||||
<Button icon="user-check"></Button>
|
||||
<Button icon="user-check" color="ghost"></Button>
|
||||
<Button color="light">button.light</Button>
|
||||
<Button color="light" active>button.light.active</Button>
|
||||
</div>
|
||||
<div class="margin-bottom-3 style-guide__buttons">
|
||||
<a class="button button--large">a.button.button-large</a>
|
||||
<a class="button button--large disabled"
|
||||
>a.button.disabled.button-large</a
|
||||
>
|
||||
<a class="button button--large button--success"
|
||||
>a.button.button-large.button-success</a
|
||||
>
|
||||
<a class="button button--large button--warning"
|
||||
>a.button.button-large.button-warning</a
|
||||
>
|
||||
<a class="button button--large button--error"
|
||||
>a.button.button-large.button-error</a
|
||||
>
|
||||
<a class="button button--large">
|
||||
a.button.button-large
|
||||
<i class="button__icon fas fa-lock-open"></i>
|
||||
</a>
|
||||
<a class="button button--large">
|
||||
<i class="button__icon fas fa-arrow-left"></i>
|
||||
a.button.button-large
|
||||
</a>
|
||||
<a class="button button--large button--ghost"
|
||||
>a.button.button-ghosts</a
|
||||
>
|
||||
<a class="button button--large">
|
||||
<i class="fas fa-user-check"></i>
|
||||
</a>
|
||||
<a class="button button--large button--ghost">
|
||||
<i class="fas fa-user-check"></i>
|
||||
</a>
|
||||
<h3>Large size</h3>
|
||||
<Button size="large">button</Button>
|
||||
<Button size="large" disabled>button[disabled]</Button>
|
||||
<Button size="large" color="success">button.button-success</Button>
|
||||
<Button size="large" color="warning">button.button-warning</Button>
|
||||
<Button size="large" color="error">button.button-error</Button>
|
||||
<Button size="large" append-icon="lock-open">button</Button>
|
||||
<Button size="large" prepend-icon="arrow-left">button</Button>
|
||||
<Button size="large" color="ghost">button.button-ghost</Button>
|
||||
<Button size="large" icon="user-check"></Button>
|
||||
<Button size="large" icon="user-check" color="ghost"></Button>
|
||||
</div>
|
||||
<div class="margin-bottom-3">
|
||||
<a class="button button--large button--loading">Loading</a>
|
||||
<a class="button button--loading">Loading</a>
|
||||
<a class="button button--ghost button--loading">Loading</a>
|
||||
<div
|
||||
class="margin-bottom-3"
|
||||
:style="{ backgroundColor: 'white', padding: '5px' }"
|
||||
>
|
||||
<h3>Loading</h3>
|
||||
<Button size="tiny" loading>Loading</Button>
|
||||
<Button loading>Loading</Button>
|
||||
<Button size="large" loading>Loading</Button>
|
||||
<Button size="large" color="ghost" loading>Loading</Button>
|
||||
<Button size="large" icon="user-check" loading></Button>
|
||||
<Button color="light" loading>button.light.loading</Button>
|
||||
<Button color="light" active loading>
|
||||
button.light.active.loading
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
class="margin-bottom-3"
|
||||
style="background-color: #ffffff; padding: 20px"
|
||||
>
|
||||
<h3>Classic tabs</h3>
|
||||
<Tabs>
|
||||
<Tab :selected="true" :title="'Tab 1'">
|
||||
<p>
|
||||
|
@ -532,6 +589,41 @@
|
|||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
<div
|
||||
class="margin-bottom-3"
|
||||
style="background-color: #ffffff; padding: 20px"
|
||||
>
|
||||
<h3>No separation</h3>
|
||||
<Tabs no-separation>
|
||||
<Tab :selected="true" :title="'Tab 1'">
|
||||
<p>
|
||||
Tab 1 content Lorem ipsum dolor sit amet,
|
||||
<a href="#">consectetur</a> adipiscing elit. Sed quis gravida
|
||||
ante. Nulla nec elit dui. Nam nec dui ligula. Pellentesque
|
||||
feugiat erat vel porttitor euismod. Duis nec viverra urna.
|
||||
Praesent.
|
||||
</p>
|
||||
</Tab>
|
||||
<Tab :title="'Tab 2'">
|
||||
<p>
|
||||
Tab 2 content Lorem ipsum dolor sit amet,
|
||||
<a href="#">consectetur</a> adipiscing elit. Sed quis gravida
|
||||
ante. Nulla nec elit dui. Nam nec dui ligula. Pellentesque
|
||||
feugiat erat vel porttitor euismod. Duis nec viverra urna.
|
||||
Praesent.
|
||||
</p>
|
||||
</Tab>
|
||||
<Tab :title="'Tab 3'">
|
||||
<p>
|
||||
Tab 3 content Lorem ipsum dolor sit amet,
|
||||
<a href="#">consectetur</a> adipiscing elit. Sed quis gravida
|
||||
ante. Nulla nec elit dui. Nam nec dui ligula. Pellentesque
|
||||
feugiat erat vel porttitor euismod. Duis nec viverra urna.
|
||||
Praesent.
|
||||
</p>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<div class="margin-bottom-3">
|
||||
<div class="tooltip margin-bottom-2">
|
||||
|
@ -909,10 +1001,12 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="margin-bottom-3">
|
||||
<a class="button" @click="$refs.context1.toggle($event.target)">
|
||||
toggle context
|
||||
<i class="fas fa-pencil"></i>
|
||||
</a>
|
||||
<Button
|
||||
append-icon="pencil"
|
||||
@click="$refs.context1.toggle($event.target)"
|
||||
>
|
||||
Toggle context
|
||||
</Button>
|
||||
<Context ref="context1">
|
||||
<div class="context__menu-title">Vehicles</div>
|
||||
<ul class="context__menu">
|
||||
|
@ -937,13 +1031,11 @@
|
|||
</li>
|
||||
<Modal ref="modal1">
|
||||
<h2 class="box__title">Modal inside a context</h2>
|
||||
<a
|
||||
class="button"
|
||||
<Button
|
||||
icon="pencil"
|
||||
@click="$refs.context3.toggle($event.target)"
|
||||
>Toggle context</Button
|
||||
>
|
||||
toggle context
|
||||
<i class="fas fa-pencil"></i>
|
||||
</a>
|
||||
<Context ref="context3">
|
||||
<div class="context__menu-title">Vehicles</div>
|
||||
<ul class="context__menu">
|
||||
|
@ -985,7 +1077,7 @@
|
|||
</li>
|
||||
</ul>
|
||||
</Context>
|
||||
<a class="button" @click="$refs.modal2.show()">show modal</a>
|
||||
<Button @click="$refs.modal2.show()">show modal</Button>
|
||||
<Modal ref="modal2">
|
||||
<h2 class="box__title">An example modal</h2>
|
||||
<p>
|
||||
|
@ -1002,46 +1094,49 @@
|
|||
fringilla. Praesent ut tincidunt dui.
|
||||
</p>
|
||||
</Modal>
|
||||
<a
|
||||
class="button button--success"
|
||||
<Button
|
||||
color="success"
|
||||
@click="
|
||||
$store.dispatch('notification/success', {
|
||||
title: 'Custom success notification',
|
||||
message: 'Mauris dignissim massa ac justo consequat porttitor.',
|
||||
})
|
||||
"
|
||||
>toggle success notification</a
|
||||
>
|
||||
<a
|
||||
class="button button--error"
|
||||
toggle success notification
|
||||
</Button>
|
||||
<Button
|
||||
color="error"
|
||||
@click="
|
||||
$store.dispatch('notification/error', {
|
||||
title: 'Custom error notification',
|
||||
message: 'Mauris dignissim massa ac justo consequat porttitor.',
|
||||
})
|
||||
"
|
||||
>toggle error notification</a
|
||||
>
|
||||
<a
|
||||
class="button button--warning"
|
||||
toggle error notification
|
||||
</Button>
|
||||
<Button
|
||||
color="warning"
|
||||
@click="
|
||||
$store.dispatch('notification/warning', {
|
||||
title: 'Custom warning notification',
|
||||
message: 'Mauris dignissim massa ac justo consequat porttitor.',
|
||||
})
|
||||
"
|
||||
>toggle warning notification</a
|
||||
>
|
||||
<a
|
||||
class="button"
|
||||
toggle warning notification
|
||||
</Button>
|
||||
<Button
|
||||
@click="
|
||||
$store.dispatch('notification/info', {
|
||||
title: 'Custom info notification',
|
||||
message: 'Mauris dignissim massa ac justo consequat porttitor.',
|
||||
})
|
||||
"
|
||||
>toggle info notification</a
|
||||
>
|
||||
toggle info notification
|
||||
</Button>
|
||||
</div>
|
||||
<div class="margin-bottom-3">
|
||||
<div class="modal__box">
|
||||
|
@ -1089,9 +1184,7 @@
|
|||
</ul>
|
||||
</div>
|
||||
<div class="actions actions--right">
|
||||
<button class="button button--large button--overflow">
|
||||
Validate
|
||||
</button>
|
||||
<Button size="large" overflow>Validate</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__box modal__box--with-sidebar">
|
||||
|
@ -1350,9 +1443,9 @@
|
|||
/>
|
||||
<i class="fas fa-search"></i>
|
||||
</div>
|
||||
<a href="#" class="button button--large margin-left-2"
|
||||
>Invite member</a
|
||||
>
|
||||
<Button href="#" size="large" class="margin-left-2">
|
||||
Invite member
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-table__body">
|
||||
|
@ -1504,6 +1597,7 @@ export default {
|
|||
return {
|
||||
checkbox: false,
|
||||
radio: 'a',
|
||||
radioButton: 'a',
|
||||
switchValue: false,
|
||||
switchUnknown: 2,
|
||||
dropdown: '',
|
||||
|
|
|
@ -21,6 +21,7 @@ import Tabs from '@baserow/modules/core/components/Tabs'
|
|||
import Tab from '@baserow/modules/core/components/Tab'
|
||||
import List from '@baserow/modules/core/components/List'
|
||||
import HelpIcon from '@baserow/modules/core/components/HelpIcon'
|
||||
import Button from '@baserow/modules/core/components/Button'
|
||||
|
||||
import lowercase from '@baserow/modules/core/filters/lowercase'
|
||||
import uppercase from '@baserow/modules/core/filters/uppercase'
|
||||
|
@ -37,6 +38,7 @@ import clickOutside from '@baserow/modules/core/directives/clickOutside'
|
|||
import Badge from '@baserow/modules/core/components/Badge'
|
||||
import InputWithIcon from '@baserow/modules/core/components/InputWithIcon'
|
||||
import ExpandableCard from '@baserow/modules/core/components/ExpandableCard'
|
||||
import RadioButton from '@baserow/modules/core/components/RadioButton'
|
||||
|
||||
function setupVue(Vue) {
|
||||
Vue.component('Context', Context)
|
||||
|
@ -63,6 +65,8 @@ function setupVue(Vue) {
|
|||
Vue.component('Badge', Badge)
|
||||
Vue.component('InputWithIcon', InputWithIcon)
|
||||
Vue.component('ExpandableCard', ExpandableCard)
|
||||
Vue.component('Button', Button)
|
||||
Vue.component('RadioButton', RadioButton)
|
||||
|
||||
Vue.filter('lowercase', lowercase)
|
||||
Vue.filter('uppercase', uppercase)
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
import * as elementTypes from '@baserow/modules/builder/elementTypes'
|
||||
|
||||
const getPropsOfComponent = (component) => {
|
||||
let props = Object.keys(component.props || [])
|
||||
|
||||
if (component.mixins) {
|
||||
component.mixins.forEach((mixin) => {
|
||||
props = props.concat(Object.keys(mixin.props || []))
|
||||
})
|
||||
}
|
||||
|
||||
return props
|
||||
}
|
||||
|
||||
describe('elementTypes', () => {
|
||||
test.each(Object.values(elementTypes))(
|
||||
'test that properties mapped for the element type exist on the component as prop',
|
||||
(ElementType) => {
|
||||
const elementType = new ElementType(expect.anything())
|
||||
|
||||
if (elementType.component) {
|
||||
const propsInMapping = Object.keys(
|
||||
elementType.getComponentProps(expect.anything())
|
||||
)
|
||||
|
||||
const propsOnComponent = getPropsOfComponent(elementType.component)
|
||||
|
||||
propsInMapping.forEach((prop) => {
|
||||
expect(propsOnComponent).toContain(prop)
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
Loading…
Add table
Add a link
Reference in a new issue