diff --git a/backend/src/baserow/contrib/builder/apps.py b/backend/src/baserow/contrib/builder/apps.py index 2bb6a933f..174a5a54a 100644 --- a/backend/src/baserow/contrib/builder/apps.py +++ b/backend/src/baserow/contrib/builder/apps.py @@ -186,6 +186,7 @@ class BuilderConfig(AppConfig): MenuElementType, RecordSelectorElementType, RepeatElementType, + SimpleContainerElementType, TableElementType, TextElementType, ) @@ -209,6 +210,7 @@ class BuilderConfig(AppConfig): element_type_registry.register(HeaderElementType()) element_type_registry.register(FooterElementType()) element_type_registry.register(MenuElementType()) + element_type_registry.register(SimpleContainerElementType()) from .domains.domain_types import CustomDomainType, SubDomainType from .domains.registries import domain_type_registry diff --git a/backend/src/baserow/contrib/builder/elements/element_types.py b/backend/src/baserow/contrib/builder/elements/element_types.py index 51a8655e4..6849e4858 100644 --- a/backend/src/baserow/contrib/builder/elements/element_types.py +++ b/backend/src/baserow/contrib/builder/elements/element_types.py @@ -61,6 +61,7 @@ from baserow.contrib.builder.elements.models import ( NavigationElementMixin, RecordSelectorElement, RepeatElement, + SimpleContainerElement, TableElement, TextElement, VerticalAlignments, @@ -278,6 +279,17 @@ class FormContainerElementType(ContainerElementTypeMixin, ElementType): ] +class SimpleContainerElementType(ContainerElementTypeMixin, ElementType): + type = "simple_container" + model_class = SimpleContainerElement + + class SerializedDict(ContainerElementTypeMixin.SerializedDict): + pass + + def get_pytest_params(self, pytest_data_fixture) -> Dict[str, Any]: + return {} + + class TableElementType(CollectionElementWithFieldsTypeMixin, ElementType): type = "table" model_class = TableElement diff --git a/backend/src/baserow/contrib/builder/elements/models.py b/backend/src/baserow/contrib/builder/elements/models.py index 517bc055e..62d2800cd 100644 --- a/backend/src/baserow/contrib/builder/elements/models.py +++ b/backend/src/baserow/contrib/builder/elements/models.py @@ -1066,3 +1066,9 @@ class MenuElement(Element): ) menu_items = models.ManyToManyField(MenuItemElement) + + +class SimpleContainerElement(ContainerElement): + """ + A simple container to group elements + """ diff --git a/backend/src/baserow/contrib/builder/migrations/0054_simplecontainerelement.py b/backend/src/baserow/contrib/builder/migrations/0054_simplecontainerelement.py new file mode 100644 index 000000000..8b2b91b4f --- /dev/null +++ b/backend/src/baserow/contrib/builder/migrations/0054_simplecontainerelement.py @@ -0,0 +1,36 @@ +# Generated by Django 5.0.9 on 2025-03-08 13:26 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ( + "builder", + "0053_buttonthemeconfigblock_button_active_background_color_and_more", + ), + ] + + operations = [ + migrations.CreateModel( + name="SimpleContainerElement", + 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", + ), + ), + ], + options={ + "abstract": False, + }, + bases=("builder.element",), + ), + ] diff --git a/changelog/entries/unreleased/feature/3458_add_simple_container_element_to_group_multiple_elements.json b/changelog/entries/unreleased/feature/3458_add_simple_container_element_to_group_multiple_elements.json new file mode 100644 index 000000000..b69086cec --- /dev/null +++ b/changelog/entries/unreleased/feature/3458_add_simple_container_element_to_group_multiple_elements.json @@ -0,0 +1,8 @@ +{ + "type": "feature", + "message": "Added a container element to group multiple elements.", + "domain": "builder", + "issue_number": 3458, + "bullet_points": [], + "created_at": "2025-03-08" +} diff --git a/enterprise/web-frontend/modules/baserow_enterprise/components/EnterpriseSettings.vue b/enterprise/web-frontend/modules/baserow_enterprise/components/EnterpriseSettings.vue index 396d7e707..c59d2675e 100644 --- a/enterprise/web-frontend/modules/baserow_enterprise/components/EnterpriseSettings.vue +++ b/enterprise/web-frontend/modules/baserow_enterprise/components/EnterpriseSettings.vue @@ -84,6 +84,11 @@ import { useVuelidate } from '@vuelidate/core' export default { name: 'EnterpriseSettings', components: { UserFilesModal }, + setup() { + return { + v$: useVuelidate({ $lazy: true }), + } + }, computed: { IMAGE_FILE_TYPES() { return IMAGE_FILE_TYPES @@ -95,11 +100,6 @@ export default { settings: 'settings/get', }), }, - setup() { - return { - v$: useVuelidate({ $lazy: true }), - } - }, methods: { async updateSettings(values) { this.v$.$touch() diff --git a/web-frontend/modules/builder/assets/icons/element-simple_container.svg b/web-frontend/modules/builder/assets/icons/element-simple_container.svg new file mode 100644 index 000000000..3a1b1c22d --- /dev/null +++ b/web-frontend/modules/builder/assets/icons/element-simple_container.svg @@ -0,0 +1,5 @@ +<svg width="72" height="48" viewBox="0 0 72 48" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect x="1.25" y="1.25" width="69.5" height="45.5" rx="4.75" fill="white"/> +<rect x="1.25" y="1.25" width="69.5" height="45.5" rx="4.75" stroke="#E6E6E7" stroke-width="1.5"/> +<rect x="8.75" y="8.75" width="54.5" height="30.5" rx="3.25" fill="#F7F7F7" stroke="#E6E6E7" stroke-width="1.5" stroke-dasharray="4 4"/> +</svg> diff --git a/web-frontend/modules/builder/components/elements/components/SimpleContainerElement.vue b/web-frontend/modules/builder/components/elements/components/SimpleContainerElement.vue new file mode 100644 index 000000000..8dbec1740 --- /dev/null +++ b/web-frontend/modules/builder/components/elements/components/SimpleContainerElement.vue @@ -0,0 +1,66 @@ +<template> + <div> + <template + v-if=" + mode === 'editing' && + children.length === 0 && + $hasPermission('builder.page.create_element', currentPage, workspace.id) + " + > + <AddElementZone @add-element="showAddElementModal"></AddElementZone> + <AddElementModal + ref="addElementModal" + :page="elementPage" + ></AddElementModal> + </template> + <template v-else> + <template v-for="child in children"> + <ElementPreview + v-if="mode === 'editing'" + :key="child.id" + :element="child" + @move="$emit('move', $event)" + /> + <PageElement + v-else + :key="`${child.id}else`" + :element="child" + :mode="mode" + /> + </template> + </template> + </div> +</template> + +<script> +import AddElementZone from '@baserow/modules/builder/components/elements/AddElementZone' +import containerElement from '@baserow/modules/builder/mixins/containerElement' +import AddElementModal from '@baserow/modules/builder/components/elements/AddElementModal' +import ElementPreview from '@baserow/modules/builder/components/elements/ElementPreview' +import PageElement from '@baserow/modules/builder/components/page/PageElement' + +export default { + name: 'SimpleContainerElement', + components: { + PageElement, + ElementPreview, + AddElementModal, + AddElementZone, + }, + mixins: [containerElement], + props: { + element: { + type: Object, + required: true, + }, + }, + methods: { + showAddElementModal() { + this.$refs.addElementModal.show({ + placeInContainer: null, + parentElementId: this.element.id, + }) + }, + }, +} +</script> diff --git a/web-frontend/modules/builder/components/elements/components/forms/general/SimpleContainerElementForm.vue b/web-frontend/modules/builder/components/elements/components/forms/general/SimpleContainerElementForm.vue new file mode 100644 index 000000000..9fbfc74dd --- /dev/null +++ b/web-frontend/modules/builder/components/elements/components/forms/general/SimpleContainerElementForm.vue @@ -0,0 +1,22 @@ +<template> + <form @submit.prevent @keydown.enter.prevent> + <p>{{ $t('simpleContainerElementForm.noConfigurationOptions') }}</p> + </form> +</template> + +<script> +import elementForm from '@baserow/modules/builder/mixins/elementForm' + +export default { + name: 'SimpleContainerElementForm', + mixins: [elementForm], + data() { + return { + values: { + styles: {}, + }, + allowedValues: ['styles'], + } + }, +} +</script> diff --git a/web-frontend/modules/builder/components/page/settings/PageSettingsQueryParamsFormElement.vue b/web-frontend/modules/builder/components/page/settings/PageSettingsQueryParamsFormElement.vue index faae03e6d..b527f8b34 100644 --- a/web-frontend/modules/builder/components/page/settings/PageSettingsQueryParamsFormElement.vue +++ b/web-frontend/modules/builder/components/page/settings/PageSettingsQueryParamsFormElement.vue @@ -30,6 +30,7 @@ </Dropdown> </div> <ButtonIcon + tag="a" class="filters__remove page-settings-query-params__remove" icon="iconoir-bin" @click="deleteQueryParam(index)" diff --git a/web-frontend/modules/builder/elementTypes.js b/web-frontend/modules/builder/elementTypes.js index 202e8bee9..6a831fb53 100644 --- a/web-frontend/modules/builder/elementTypes.js +++ b/web-frontend/modules/builder/elementTypes.js @@ -37,6 +37,8 @@ import RuntimeFormulaContext from '@baserow/modules/core/runtimeFormulaContext' import { resolveFormula } from '@baserow/modules/core/formula' import FormContainerElement from '@baserow/modules/builder/components/elements/components/FormContainerElement.vue' import FormContainerElementForm from '@baserow/modules/builder/components/elements/components/forms/general/FormContainerElementForm.vue' +import SimpleContainerElement from '@baserow/modules/builder/components/elements/components/SimpleContainerElement.vue' +import SimpleContainerElementForm from '@baserow/modules/builder/components/elements/components/forms/general/SimpleContainerElementForm.vue' import ChoiceElement from '@baserow/modules/builder/components/elements/components/ChoiceElement.vue' import ChoiceElementForm from '@baserow/modules/builder/components/elements/components/forms/general/ChoiceElementForm.vue' import CheckboxElement from '@baserow/modules/builder/components/elements/components/CheckboxElement.vue' @@ -856,6 +858,58 @@ export class ColumnElementType extends ContainerElementTypeMixin(ElementType) { } } +export class SimpleContainerElementType extends ContainerElementTypeMixin( + ElementType +) { + static getType() { + return 'simple_container' + } + + category() { + return 'layoutElement' + } + + get name() { + return this.app.i18n.t('elementType.simpleContainer') + } + + get description() { + return this.app.i18n.t('elementType.simpleContainerDescription') + } + + get iconClass() { + return 'iconoir-square' + } + + get component() { + return SimpleContainerElement + } + + get generalFormComponent() { + return SimpleContainerElementForm + } + + getDefaultValues(page, values) { + const superValues = super.getDefaultValues(page, values) + return { + ...superValues, + style_padding_left: 0, + style_padding_right: 0, + style_padding_top: 0, + style_padding_bottom: 0, + } + } + + getDefaultChildValues(page, values) { + // Unlike other container we don't want to affect the child padding. + return {} + } + + getElementPlaces(element) { + return [null] + } +} + export class TableElementType extends CollectionElementTypeMixin(ElementType) { static getType() { return 'table' diff --git a/web-frontend/modules/builder/locales/en.json b/web-frontend/modules/builder/locales/en.json index 17c6ec0af..12ee0a2cd 100644 --- a/web-frontend/modules/builder/locales/en.json +++ b/web-frontend/modules/builder/locales/en.json @@ -125,7 +125,9 @@ "notAllowedInsideSameType": "This element is not allowed in a container of the same type", "notAllowedLocation": "This element is not allowed at this location", "menu": "Menu", - "menuDescription": "Menu element" + "menuDescription": "Menu element", + "simpleContainer": "Container", + "simpleContainerDescription": "A container for other elements" }, "addElementButton": { "label": "Element" @@ -232,6 +234,9 @@ "eventDescription": "To configure actions for this button, open the Events tab of this element.", "noMenuItemsMessage": "Click 'Add' to add your first menu item." }, + "simpleContainerElementForm": { + "noConfigurationOptions": "The container element does not have any configuration options." + }, "imageElement": { "missingValue": "Missing alt text...", "emptyValue": "Empty alt text..." diff --git a/web-frontend/modules/builder/plugin.js b/web-frontend/modules/builder/plugin.js index 055674e7c..3b6eb6398 100644 --- a/web-frontend/modules/builder/plugin.js +++ b/web-frontend/modules/builder/plugin.js @@ -45,6 +45,7 @@ import { HeaderElementType, FooterElementType, MenuElementType, + SimpleContainerElementType, } from '@baserow/modules/builder/elementTypes' import { DesktopDeviceType, @@ -218,6 +219,7 @@ export default (context) => { app.$registry.register('element', new LinkElementType(context)) app.$registry.register('element', new ButtonElementType(context)) app.$registry.register('element', new TableElementType(context)) + app.$registry.register('element', new SimpleContainerElementType(context)) app.$registry.register('element', new ColumnElementType(context)) app.$registry.register('element', new HeaderElementType(context)) app.$registry.register('element', new FooterElementType(context)) diff --git a/web-frontend/modules/core/assets/scss/components/builder/add_element_icons.scss b/web-frontend/modules/core/assets/scss/components/builder/add_element_icons.scss index c1ce27398..c3dc3c942 100644 --- a/web-frontend/modules/core/assets/scss/components/builder/add_element_icons.scss +++ b/web-frontend/modules/core/assets/scss/components/builder/add_element_icons.scss @@ -13,6 +13,7 @@ $baserow-builder-icons: ( 'header', 'heading', 'iframe', + 'simple_container', 'positioned_container', 'menu', 'image',