1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-06 14:05:28 +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" type = "text"
model_class = TextElement model_class = TextElement
serializer_field_names = ["value", "alignment"] serializer_field_names = ["value", "alignment", "format"]
allowed_fields = ["value", "alignment"] allowed_fields = ["value", "alignment", "format"]
class SerializedDict(ElementDict): class SerializedDict(ElementDict):
value: BaserowFormula value: BaserowFormula
alignment: str alignment: str
format: str
def get_pytest_params(self, pytest_data_fixture): def get_pytest_params(self, pytest_data_fixture):
return { return {
@ -466,6 +467,7 @@ class TextElementType(ElementType):
"Asperiores corporis perspiciatis nam harum veritatis. " "Asperiores corporis perspiciatis nam harum veritatis. "
"Impedit qui maxime aut illo quod ea molestias.'", "Impedit qui maxime aut illo quod ea molestias.'",
"alignment": "left", "alignment": "left",
"format": TextElement.TEXT_FORMATS.PLAIN,
} }
@property @property
@ -479,6 +481,11 @@ class TextElementType(ElementType):
allow_blank=True, allow_blank=True,
default="", 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): def import_serialized(self, page, serialized_values, id_mapping):

View file

@ -355,12 +355,22 @@ class TextElement(Element):
A simple blob of text. A simple blob of text.
""" """
class TEXT_FORMATS(models.TextChoices):
PLAIN = "plain"
MARKDOWN = "markdown"
value = FormulaField(default="") value = FormulaField(default="")
alignment = models.CharField( alignment = models.CharField(
choices=HorizontalAlignments.choices, choices=HorizontalAlignments.choices,
max_length=10, max_length=10,
default=HorizontalAlignments.LEFT, 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): 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", "style_background": "none",
"value": element2.value, "value": element2.value,
"alignment": "left", "alignment": "left",
"format": TextElement.TEXT_FORMATS.PLAIN,
}, },
{ {
"id": element_container.id, "id": element_container.id,
@ -277,6 +278,7 @@ def test_builder_application_export(data_fixture):
"order": str(element_inside_container.order), "order": str(element_inside_container.order),
"value": element_inside_container.value, "value": element_inside_container.value,
"alignment": "left", "alignment": "left",
"format": TextElement.TEXT_FORMATS.PLAIN,
}, },
], ],
}, },

View file

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

View file

@ -6,12 +6,28 @@
[`element--alignment-horizontal-${element.alignment}`]: true, [`element--alignment-horizontal-${element.alignment}`]: true,
}" }"
> >
<p v-for="paragraph in paragraphs" :key="paragraph.id" class="ab-paragraph"> <template v-if="element.format === TEXT_FORMAT_TYPES.MARKDOWN">
{{ paragraph.content }} <MarkdownIt
</p> v-if="resolvedValue"
<p v-if="!paragraphs.length" class="ab-paragraph"> class="ab-paragraph"
{{ $t('textElement.noValue') }} :content="resolvedValue"
</p> :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> </div>
</template> </template>
@ -19,6 +35,8 @@
import element from '@baserow/modules/builder/mixins/element' import element from '@baserow/modules/builder/mixins/element'
import { generateHash } from '@baserow/modules/core/utils/hashing' import { generateHash } from '@baserow/modules/core/utils/hashing'
import { ensureString } from '@baserow/modules/core/utils/validator' 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 * @typedef Text
@ -34,6 +52,7 @@ export default {
* @type {Object} * @type {Object}
* @property {Array.<Text>} value - A list of paragraphs * @property {Array.<Text>} value - A list of paragraphs
* @property {string} alignment - The alignment of the element on the page * @property {string} alignment - The alignment of the element on the page
* @property {string} format - The format of the text
*/ */
element: { element: {
type: Object, type: Object,
@ -58,6 +77,77 @@ export default {
id: generateHash(line + index), 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> </script>

View file

@ -9,13 +9,24 @@
<FormElement class="control"> <FormElement class="control">
<HorizontalAlignmentsSelector v-model="values.alignment" /> <HorizontalAlignmentsSelector v-model="values.alignment" />
</FormElement> </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> </form>
</template> </template>
<script> <script>
import ApplicationBuilderFormulaInputGroup from '@baserow/modules/builder/components/ApplicationBuilderFormulaInputGroup' import ApplicationBuilderFormulaInputGroup from '@baserow/modules/builder/components/ApplicationBuilderFormulaInputGroup'
import elementForm from '@baserow/modules/builder/mixins/elementForm' 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' import HorizontalAlignmentsSelector from '@baserow/modules/builder/components/elements/components/forms/general/settings/HorizontalAlignmentsSelector.vue'
export default { export default {
@ -30,8 +41,14 @@ export default {
values: { values: {
value: '', value: '',
alignment: HORIZONTAL_ALIGNMENTS.LEFT.value, alignment: HORIZONTAL_ALIGNMENTS.LEFT.value,
format: TEXT_FORMAT_TYPES.PLAIN,
}, },
} }
}, },
computed: {
TEXT_FORMAT_TYPES() {
return TEXT_FORMAT_TYPES
},
},
} }
</script> </script>

View file

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

View file

@ -1,5 +1,5 @@
<template> <template>
<div <ThemeProvider
class="page-preview__wrapper" class="page-preview__wrapper"
@click.self="actionSelectElement({ element: null })" @click.self="actionSelectElement({ element: null })"
> >
@ -32,7 +32,7 @@
/> />
</div> </div>
</div> </div>
</div> </ThemeProvider>
</template> </template>
<script> <script>
@ -42,10 +42,12 @@ import { notifyIf } from '@baserow/modules/core/utils/error'
import PreviewNavigationBar from '@baserow/modules/builder/components/page/PreviewNavigationBar' import PreviewNavigationBar from '@baserow/modules/builder/components/page/PreviewNavigationBar'
import { PLACEMENTS } from '@baserow/modules/builder/enums' import { PLACEMENTS } from '@baserow/modules/builder/enums'
import AddElementModal from '@baserow/modules/builder/components/elements/AddElementModal.vue' import AddElementModal from '@baserow/modules/builder/components/elements/AddElementModal.vue'
import ThemeProvider from '@baserow/modules/builder/components/theme/ThemeProvider.vue'
export default { export default {
name: 'PagePreview', name: 'PagePreview',
components: { components: {
ThemeProvider,
AddElementModal, AddElementModal,
ElementPreview, ElementPreview,
PreviewNavigationBar, 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, text: ensureNonEmptyString,
} }
export const TEXT_FORMAT_TYPES = {
PLAIN: 'plain',
MARKDOWN: 'markdown',
}
export const IMAGE_SOURCE_TYPES = { export const IMAGE_SOURCE_TYPES = {
UPLOAD: 'upload', UPLOAD: 'upload',
URL: 'url', URL: 'url',

View file

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

View file

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

View file

@ -131,6 +131,25 @@
.api-docs__content { .api-docs__content {
color: $color-neutral-900; 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 { .api-docs__heading-wrapper {

View file

@ -2,7 +2,6 @@
margin: 0; margin: 0;
text-align: left; text-align: left;
color: var(--color, $black); color: var(--color, $black);
font-size: var(--font-size, 30px);
.element--alignment-horizontal-center & { .element--alignment-horizontal-center & {
text-align: center; text-align: center;
@ -13,29 +12,33 @@
} }
} }
// These tag specific selectors will be removed once the heading element has it's own .ab-heading--h1 {
// custom styling. color: var(--heading-h1--color, $black);
h1.heading-element__heading { font-size: var(--heading-h1--font-size, 30px);
font-size: var(--font-size, 30px);
} }
h2.heading-element__heading { .ab-heading--h2 {
font-size: var(--font-size, 26px); color: var(--heading-h2--color, $black);
font-size: var(--heading-h2--font-size, 26px);
} }
h3.heading-element__heading { .ab-heading--h3 {
font-size: var(--font-size, 22px); color: var(--heading-h3--color, $black);
font-size: var(--heading-h3--font-size, 22px);
} }
h4.heading-element__heading { .ab-heading--h4 {
font-size: var(--font-size, 18px); color: var(--heading-h4--color, $black);
font-size: var(--heading-h4--font-size, 18px);
} }
h5.heading-element__heading { .ab-heading--h5 {
font-size: var(--font-size, 14px); color: var(--heading-h5--color, $black);
font-size: var(--heading-h5--font-size, 14px);
} }
h6.heading-element__heading { .ab-heading--h6 {
font-size: var(--font-size, 14px); color: var(--heading-h6--color, $black);
font-size: var(--heading-h6--font-size, 14px);
font-style: italic; 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, type: String,
default: 'div', default: 'div',
}, },
rules: {
required: false,
type: Object,
default: () => {},
},
}, },
data() { data() {
return { return {
@ -38,6 +43,7 @@ export default {
async created() { async created() {
const Markdown = (await import('markdown-it')).default const Markdown = (await import('markdown-it')).default
this.md = new Markdown() this.md = new Markdown()
this.md.renderer.rules = { ...this.md.renderer.rules, ...this.rules }
this.htmlContent = this.md.render(this.content) this.htmlContent = this.md.render(this.content)
}, },
} }