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

Resolve "Add an option to enter Markdown for the paragraph element"

This commit is contained in:
Afonso Silva 2024-01-31 16:22:33 +00:00
parent 5aa4260435
commit 124fb9aa23
17 changed files with 250 additions and 54 deletions
backend
src/baserow/contrib/builder
tests/baserow/contrib/builder
web-frontend/modules

View file

@ -451,12 +451,13 @@ class TextElementType(ElementType):
type = "text"
model_class = TextElement
serializer_field_names = ["value", "alignment"]
allowed_fields = ["value", "alignment"]
serializer_field_names = ["value", "alignment", "format"]
allowed_fields = ["value", "alignment", "format"]
class SerializedDict(ElementDict):
value: BaserowFormula
alignment: str
format: str
def get_pytest_params(self, pytest_data_fixture):
return {
@ -466,6 +467,7 @@ class TextElementType(ElementType):
"Asperiores corporis perspiciatis nam harum veritatis. "
"Impedit qui maxime aut illo quod ea molestias.'",
"alignment": "left",
"format": TextElement.TEXT_FORMATS.PLAIN,
}
@property
@ -479,6 +481,11 @@ class TextElementType(ElementType):
allow_blank=True,
default="",
),
"format": serializers.ChoiceField(
choices=TextElement.TEXT_FORMATS.choices,
default=TextElement.TEXT_FORMATS.PLAIN,
help_text=TextElement._meta.get_field("format").help_text,
),
}
def import_serialized(self, page, serialized_values, id_mapping):

View file

@ -355,12 +355,22 @@ class TextElement(Element):
A simple blob of text.
"""
class TEXT_FORMATS(models.TextChoices):
PLAIN = "plain"
MARKDOWN = "markdown"
value = FormulaField(default="")
alignment = models.CharField(
choices=HorizontalAlignments.choices,
max_length=10,
default=HorizontalAlignments.LEFT,
)
format = models.CharField(
choices=TEXT_FORMATS.choices,
help_text="The format of the text",
max_length=10,
default=TEXT_FORMATS.PLAIN,
)
class LinkElement(Element):

View file

@ -0,0 +1,22 @@
# Generated by Django 4.0.10 on 2024-01-17 14:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("builder", "0004_image_element_sizing_styles"),
]
operations = [
migrations.AddField(
model_name="textelement",
name="format",
field=models.CharField(
choices=[("plain", "Plain"), ("markdown", "Markdown")],
default="plain",
help_text="The format of the text",
max_length=10,
),
),
]

View file

@ -228,6 +228,7 @@ def test_builder_application_export(data_fixture):
"style_background": "none",
"value": element2.value,
"alignment": "left",
"format": TextElement.TEXT_FORMATS.PLAIN,
},
{
"id": element_container.id,
@ -277,6 +278,7 @@ def test_builder_application_export(data_fixture):
"order": str(element_inside_container.order),
"value": element_inside_container.value,
"alignment": "left",
"format": TextElement.TEXT_FORMATS.PLAIN,
},
],
},

View file

@ -8,11 +8,9 @@
<component
:is="`h${element.level}`"
class="heading-element__heading"
:class="{ [`ab-heading--h${element.level}`]: true }"
:style="{
'--color': resolveColor(element.font_color, headingColorVariables),
'--font-size': `${
builder.theme[`heading_${element.level}_font_size`]
}px`,
}"
>
{{ resolvedValue || $t('headingElement.noValue') }}

View file

@ -6,12 +6,28 @@
[`element--alignment-horizontal-${element.alignment}`]: true,
}"
>
<p v-for="paragraph in paragraphs" :key="paragraph.id" class="ab-paragraph">
{{ paragraph.content }}
</p>
<p v-if="!paragraphs.length" class="ab-paragraph">
{{ $t('textElement.noValue') }}
</p>
<template v-if="element.format === TEXT_FORMAT_TYPES.MARKDOWN">
<MarkdownIt
v-if="resolvedValue"
class="ab-paragraph"
:content="resolvedValue"
:rules="rules"
@click.native="onClick"
></MarkdownIt>
<p v-else class="ab-paragraph">{{ $t('textElement.noValue') }}</p>
</template>
<template v-else>
<p
v-for="paragraph in paragraphs"
:key="paragraph.id"
class="ab-paragraph"
>
{{ paragraph.content }}
</p>
<p v-if="!paragraphs.length" class="ab-paragraph">
{{ $t('textElement.noValue') }}
</p>
</template>
</div>
</template>
@ -19,6 +35,8 @@
import element from '@baserow/modules/builder/mixins/element'
import { generateHash } from '@baserow/modules/core/utils/hashing'
import { ensureString } from '@baserow/modules/core/utils/validator'
import { TEXT_FORMAT_TYPES } from '@baserow/modules/builder/enums'
import { prefixInternalResolvedUrl } from '@baserow/modules/builder/utils/urlResolution'
/**
* @typedef Text
@ -34,6 +52,7 @@ export default {
* @type {Object}
* @property {Array.<Text>} value - A list of paragraphs
* @property {string} alignment - The alignment of the element on the page
* @property {string} format - The format of the text
*/
element: {
type: Object,
@ -58,6 +77,77 @@ export default {
id: generateHash(line + index),
}))
},
TEXT_FORMAT_TYPES() {
return TEXT_FORMAT_TYPES
},
// Custom rules to pass down to `MarkdownIt` element.
// The goal is to make the styling of the rendered markdown content
// consistent with the rest of the application builder CSS classes
rules() {
return {
heading_open: (tokens, idx, options, env, renderer) => {
const level = tokens[idx].markup.length
tokens[idx].attrJoin('class', `ab-heading--h${level}`)
return renderer.renderToken(tokens, idx, options)
},
link_open: (tokens, idx, options, env, renderer) => {
const url = prefixInternalResolvedUrl(
tokens[idx].attrGet('href'),
this.builder,
'custom',
this.mode
)
tokens[idx].attrSet('href', url)
tokens[idx].attrJoin('class', 'link-element__link')
return renderer.renderToken(tokens, idx, options)
},
image: (tokens, idx, options, env, renderer) => {
tokens[idx].attrJoin('class', 'image_element__img')
return renderer.renderToken(tokens, idx, options)
},
paragraph_open: (tokens, idx, options, env, renderer) => {
tokens[idx].attrJoin('class', 'ab-paragraph')
return renderer.renderToken(tokens, idx, options)
},
table_open: (tokens, idx, options, env, renderer) => {
tokens[idx].attrJoin('class', 'baserow-table')
return renderer.renderToken(tokens, idx, options)
},
tr_open: (tokens, idx, options, env, renderer) => {
// Only apply this styling to the first row present in table header.
if (idx > 0 && tokens[idx - 1].type === 'thead_open') {
tokens[idx].attrJoin('class', 'baserow-table__header-row')
} else {
tokens[idx].attrJoin('class', 'baserow-table__row')
}
return renderer.renderToken(tokens, idx, options)
},
th_open: (tokens, idx, options, env, renderer) => {
tokens[idx].attrJoin('class', 'baserow-table__header-cell')
return renderer.renderToken(tokens, idx, options)
},
td_open: (tokens, idx, options, env, renderer) => {
tokens[idx].attrJoin('class', 'baserow-table__cell')
return renderer.renderToken(tokens, idx, options)
},
}
},
},
methods: {
onClick(event) {
if (this.mode === 'editing') {
event.preventDefault()
return
}
if (event.target.classList.contains('link-element__link')) {
const url = event.target.getAttribute('href')
if (url.startsWith('/')) {
event.preventDefault()
this.$router.push(url)
}
}
},
},
}
</script>

View file

@ -9,13 +9,24 @@
<FormElement class="control">
<HorizontalAlignmentsSelector v-model="values.alignment" />
</FormElement>
<FormGroup :label="$t('textElementForm.textFormatTypeLabel')">
<RadioButton v-model="values.format" :value="TEXT_FORMAT_TYPES.PLAIN">
{{ $t('textElementForm.textFormatTypePlain') }}
</RadioButton>
<RadioButton v-model="values.format" :value="TEXT_FORMAT_TYPES.MARKDOWN">
{{ $t('textElementForm.textFormatTypeMarkdown') }}
</RadioButton>
</FormGroup>
</form>
</template>
<script>
import ApplicationBuilderFormulaInputGroup from '@baserow/modules/builder/components/ApplicationBuilderFormulaInputGroup'
import elementForm from '@baserow/modules/builder/mixins/elementForm'
import { HORIZONTAL_ALIGNMENTS } from '@baserow/modules/builder/enums'
import {
HORIZONTAL_ALIGNMENTS,
TEXT_FORMAT_TYPES,
} from '@baserow/modules/builder/enums'
import HorizontalAlignmentsSelector from '@baserow/modules/builder/components/elements/components/forms/general/settings/HorizontalAlignmentsSelector.vue'
export default {
@ -30,8 +41,14 @@ export default {
values: {
value: '',
alignment: HORIZONTAL_ALIGNMENTS.LEFT.value,
format: TEXT_FORMAT_TYPES.PLAIN,
},
}
},
computed: {
TEXT_FORMAT_TYPES() {
return TEXT_FORMAT_TYPES
},
},
}
</script>

View file

@ -1,19 +1,20 @@
<template>
<div>
<ThemeProvider>
<PageElement
v-for="element in elements"
:key="element.id"
:element="element"
:mode="mode"
/>
</div>
</ThemeProvider>
</template>
<script>
import PageElement from '@baserow/modules/builder/components/page/PageElement'
import ThemeProvider from '@baserow/modules/builder/components/theme/ThemeProvider.vue'
export default {
components: { PageElement },
components: { ThemeProvider, PageElement },
inject: ['builder', 'mode'],
props: {
page: {

View file

@ -1,5 +1,5 @@
<template>
<div
<ThemeProvider
class="page-preview__wrapper"
@click.self="actionSelectElement({ element: null })"
>
@ -32,7 +32,7 @@
/>
</div>
</div>
</div>
</ThemeProvider>
</template>
<script>
@ -42,10 +42,12 @@ import { notifyIf } from '@baserow/modules/core/utils/error'
import PreviewNavigationBar from '@baserow/modules/builder/components/page/PreviewNavigationBar'
import { PLACEMENTS } from '@baserow/modules/builder/enums'
import AddElementModal from '@baserow/modules/builder/components/elements/AddElementModal.vue'
import ThemeProvider from '@baserow/modules/builder/components/theme/ThemeProvider.vue'
export default {
name: 'PagePreview',
components: {
ThemeProvider,
AddElementModal,
ElementPreview,
PreviewNavigationBar,

View file

@ -0,0 +1,32 @@
<template>
<div :style="style">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'ThemeProvider',
inject: ['builder'],
computed: {
style() {
const colors = {
'--primary-color': this.builder.theme.primary_color,
'--secondary-color': this.builder.theme.secondary_color,
}
const headings = Array.from([1, 2, 3, 4, 5, 6]).reduce(
(headings, level) => ({
[`--heading-h${level}--font-size`]: `${
this.builder.theme[`heading_${level}_font_size`]
}px`,
[`--heading-h${level}--color`]:
this.builder.theme[`heading_${level}_color`],
...headings,
}),
{}
)
return { ...colors, ...headings }
},
},
}
</script>

View file

@ -20,6 +20,11 @@ export const PAGE_PARAM_TYPE_VALIDATION_FUNCTIONS = {
text: ensureNonEmptyString,
}
export const TEXT_FORMAT_TYPES = {
PLAIN: 'plain',
MARKDOWN: 'markdown',
}
export const IMAGE_SOURCE_TYPES = {
UPLOAD: 'upload',
URL: 'url',

View file

@ -143,7 +143,10 @@
"textElementForm": {
"textTitle": "Text",
"textPlaceholder": "Enter text...",
"textError": "The value is invalid."
"textError": "The value is invalid.",
"textFormatTypeLabel": "Format",
"textFormatTypePlain": "Plain text",
"textFormatTypeMarkdown": "Markdown"
},
"imageElement": {
"emptyState": "No alt text defined..."

View file

@ -19,7 +19,6 @@
@import 'dropdown';
@import 'field_form_context';
@import 'tooltip';
@import 'markdown';
@import 'rating';
@import 'progress';
@import 'fields/boolean';

View file

@ -131,6 +131,25 @@
.api-docs__content {
color: $color-neutral-900;
&.markdown code {
display: inline-block;
font-size: 14px;
padding: 3px 6px;
background-color: $color-neutral-100;
@include rounded($rounded);
}
&.markdown p {
color: $color-neutral-900;
margin-bottom: 20px;
font-size: 13px;
line-height: 140%;
}
&.markdown ul {
padding-left: 1em;
}
}
.api-docs__heading-wrapper {

View file

@ -2,7 +2,6 @@
margin: 0;
text-align: left;
color: var(--color, $black);
font-size: var(--font-size, 30px);
.element--alignment-horizontal-center & {
text-align: center;
@ -13,29 +12,33 @@
}
}
// These tag specific selectors will be removed once the heading element has it's own
// custom styling.
h1.heading-element__heading {
font-size: var(--font-size, 30px);
.ab-heading--h1 {
color: var(--heading-h1--color, $black);
font-size: var(--heading-h1--font-size, 30px);
}
h2.heading-element__heading {
font-size: var(--font-size, 26px);
.ab-heading--h2 {
color: var(--heading-h2--color, $black);
font-size: var(--heading-h2--font-size, 26px);
}
h3.heading-element__heading {
font-size: var(--font-size, 22px);
.ab-heading--h3 {
color: var(--heading-h3--color, $black);
font-size: var(--heading-h3--font-size, 22px);
}
h4.heading-element__heading {
font-size: var(--font-size, 18px);
.ab-heading--h4 {
color: var(--heading-h4--color, $black);
font-size: var(--heading-h4--font-size, 18px);
}
h5.heading-element__heading {
font-size: var(--font-size, 14px);
.ab-heading--h5 {
color: var(--heading-h5--color, $black);
font-size: var(--heading-h5--font-size, 14px);
}
h6.heading-element__heading {
font-size: var(--font-size, 14px);
.ab-heading--h6 {
color: var(--heading-h6--color, $black);
font-size: var(--heading-h6--font-size, 14px);
font-style: italic;
}

View file

@ -1,20 +0,0 @@
.markdown {
& code {
display: inline-block;
font-size: 14px;
padding: 3px 6px;
background-color: $color-neutral-100;
@include rounded($rounded);
}
& p {
color: $color-neutral-900;
margin-bottom: 20px;
font-size: 13px;
line-height: 140%;
}
& ul {
padding-left: 1em;
}
}

View file

@ -16,6 +16,11 @@ export default {
type: String,
default: 'div',
},
rules: {
required: false,
type: Object,
default: () => {},
},
},
data() {
return {
@ -38,6 +43,7 @@ export default {
async created() {
const Markdown = (await import('markdown-it')).default
this.md = new Markdown()
this.md.renderer.rules = { ...this.md.renderer.rules, ...this.rules }
this.htmlContent = this.md.render(this.content)
},
}