1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-06 22:08:52 +00:00

Resolve "Make the table element responsive"

This commit is contained in:
Andrea Disarò 2024-06-13 12:45:52 +00:00 committed by Afonso Silva
parent ad5da1f276
commit d62e4fdb46
16 changed files with 320 additions and 82 deletions
backend
src/baserow/contrib/builder
tests/baserow/contrib/builder
changelog/entries/unreleased/feature
web-frontend/modules

View file

@ -42,6 +42,7 @@ from baserow.contrib.builder.elements.models import (
TableElement, TableElement,
TextElement, TextElement,
VerticalAlignments, VerticalAlignments,
get_default_table_orientation,
) )
from baserow.contrib.builder.elements.registries import ( from baserow.contrib.builder.elements.registries import (
ElementType, ElementType,
@ -219,14 +220,15 @@ class TableElementType(CollectionElementWithFieldsTypeMixin, ElementType):
class SerializedDict(CollectionElementWithFieldsTypeMixin.SerializedDict): class SerializedDict(CollectionElementWithFieldsTypeMixin.SerializedDict):
button_color: str button_color: str
orientation: dict
@property @property
def allowed_fields(self): def allowed_fields(self):
return super().allowed_fields + ["button_color"] return super().allowed_fields + ["button_color", "orientation"]
@property @property
def serializer_field_names(self): def serializer_field_names(self):
return super().serializer_field_names + ["button_color"] return super().serializer_field_names + ["button_color", "orientation"]
@property @property
def serializer_field_overrides(self): def serializer_field_overrides(self):
@ -238,10 +240,18 @@ class TableElementType(CollectionElementWithFieldsTypeMixin, ElementType):
default="primary", default="primary",
help_text="Button color.", help_text="Button color.",
), ),
"orientation": serializers.JSONField(
allow_null=False,
default=get_default_table_orientation,
help_text=TableElement._meta.get_field("orientation").help_text,
),
} }
def get_pytest_params(self, pytest_data_fixture) -> Dict[str, Any]: def get_pytest_params(self, pytest_data_fixture) -> Dict[str, Any]:
return {"data_source_id": None} return {
"data_source_id": None,
"orientation": get_default_table_orientation(),
}
class RepeatElementType( class RepeatElementType(

View file

@ -59,6 +59,14 @@ def get_default_element_content_type():
return ContentType.objects.get_for_model(Element) return ContentType.objects.get_for_model(Element)
def get_default_table_orientation():
return {
"smartphone": "horizontal",
"tablet": "horizontal",
"desktop": "horizontal",
}
class Element( class Element(
HierarchicalModelMixin, HierarchicalModelMixin,
TrashableModelMixin, TrashableModelMixin,
@ -765,7 +773,12 @@ class TableElement(CollectionElement):
blank=True, blank=True,
help_text="The color of the button", help_text="The color of the button",
) )
orientation = models.JSONField(
blank=True,
null=True,
default=get_default_table_orientation,
help_text="The table orientation (horizontal or vertical) for each device type",
)
fields = models.ManyToManyField(CollectionField) fields = models.ManyToManyField(CollectionField)

View file

@ -0,0 +1,39 @@
# Generated by Django 4.1.13 on 2024-06-06 11:12
from django.db import migrations, models
from baserow.contrib.builder.elements.models import get_default_table_orientation
def populate_orientation_field(apps, schema_editor):
"""Add default orientation settings to all table elements."""
TableElement = apps.get_model("builder", "tableelement")
TableElement.objects.update(orientation={
"smartphone": "horizontal",
"tablet": "horizontal",
"desktop": "horizontal",
})
class Migration(migrations.Migration):
dependencies = [
("builder", "0022_choiceelement_choiceelementoption_and_more"),
]
operations = [
migrations.AddField(
model_name="tableelement",
name="orientation",
field=models.JSONField(
blank=True,
default=get_default_table_orientation,
help_text="The table orientation (horizontal or vertical) for each device type",
null=True,
),
),
migrations.RunPython(
populate_orientation_field,
reverse_code=migrations.RunPython.noop,
),
]

View file

@ -372,6 +372,11 @@ def test_builder_application_export(data_fixture):
"type": "table", "type": "table",
"order": str(element4.order), "order": str(element4.order),
"button_color": "primary", "button_color": "primary",
"orientation": {
"smartphone": "horizontal",
"tablet": "horizontal",
"desktop": "horizontal",
},
"parent_element_id": None, "parent_element_id": None,
"place_in_container": None, "place_in_container": None,
"visibility": "all", "visibility": "all",

View file

@ -0,0 +1,7 @@
{
"type": "feature",
"message": "[Builder] Add responsive behavior configuration for table element",
"issue_number": 2523,
"bullet_points": [],
"created_at": "2024-06-05"
}

View file

@ -1,47 +1,93 @@
<template> <template>
<table class="baserow-table"> <div class="baserow-table-wrapper">
<thead> <table class="baserow-table" :class="`baserow-table--${orientation}`">
<tr class="baserow-table__header-row"> <template v-if="orientation === TABLE_ORIENTATION.HORIZONTAL">
<th <thead>
v-for="field in fields" <tr class="baserow-table__row">
:key="field.__id__" <th
class="baserow-table__header-cell" v-for="field in fields"
> :key="field.__id__"
<slot name="field-name" :field="field">{{ field.name }}</slot> class="baserow-table__header-cell"
</th> >
</tr> <slot name="field-name" :field="field">{{ field.name }}</slot>
</thead> </th>
<tbody v-if="rows.length"> </tr>
<tr </thead>
v-for="(row, index) in rows" <tbody v-if="rows.length">
:key="row.__id__" <tr
class="baserow-table__row" v-for="(row, index) in rows"
> :key="row.__id__"
<td v-for="field in fields" :key="field.id" class="baserow-table__cell"> class="baserow-table__row"
<slot
name="cell-content"
:value="row[field.name]"
:field="field"
:row-index="index"
> >
{{ value }} <td
</slot> v-for="field in fields"
</td> :key="field.id"
</tr> class="baserow-table__cell"
</tbody> >
<tbody v-else> <slot
<tr> name="cell-content"
<td class="baserow-table__empty-message" :colspan="fields.length"> :value="row[field.name]"
<slot name="empty-state"></slot> :field="field"
</td> :row-index="index"
</tr> >
</tbody> {{ value }}
</table> </slot>
</td>
</tr>
</tbody>
</template>
<template v-else>
<tbody v-if="rows.length">
<template v-for="(row, rowIndex) in rows">
<tr
v-for="(field, fieldIndex) in fields"
:key="`${row.__id__}_${field.id}`"
class="baserow-table__row"
>
<th
class="baserow-table__header-cell"
:class="{
'baserow-table__separator': fieldIndex === fields.length - 1,
}"
>
{{ field.name }}
</th>
<td
class="baserow-table__cell"
:class="{
'baserow-table__separator': fieldIndex === fields.length - 1,
}"
>
<slot
name="cell-content"
:value="row[field.name]"
:field="field"
:row-index="rowIndex"
>
{{ value }}
</slot>
</td>
</tr>
</template>
</tbody>
</template>
<tbody v-if="!rows.length">
<tr>
<td class="baserow-table__empty-message" :colspan="fields.length">
<slot name="empty-state"></slot>
</td>
</tr>
</tbody>
</table>
</div>
</template> </template>
<script> <script>
import { TABLE_ORIENTATION } from '@baserow/modules/builder/enums'
export default { export default {
name: 'BaserowTable', name: 'BaserowTable',
inject: ['mode'],
props: { props: {
fields: { fields: {
type: Array, type: Array,
@ -51,12 +97,18 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
orientation: {
type: String,
default: TABLE_ORIENTATION.HORIZONTAL,
},
}, },
data() { data() {
return {} return {}
}, },
computed: {}, computed: {
watch: {}, TABLE_ORIENTATION() {
methods: {}, return TABLE_ORIENTATION
},
},
} }
</script> </script>

View file

@ -5,7 +5,11 @@
}" }"
class="table-element" class="table-element"
> >
<BaserowTable :fields="element.fields" :rows="rows"> <BaserowTable
:fields="element.fields"
:rows="rows"
:orientation="orientation"
>
<template #cell-content="{ rowIndex, field, value }"> <template #cell-content="{ rowIndex, field, value }">
<component <component
:is="collectionFieldTypes[field.type].component" :is="collectionFieldTypes[field.type].component"
@ -55,6 +59,8 @@ export default {
* display. * display.
* @property {Object} fields - The fields of the data source. * @property {Object} fields - The fields of the data source.
* @property {int} items_per_page - The number of items per page. * @property {int} items_per_page - The number of items per page.
* @property {string} button_color - The color of the button.
* @property {string} orientation - The orientation for eaceh device.
*/ */
element: { element: {
type: Object, type: Object,
@ -94,6 +100,10 @@ export default {
collectionFieldTypes() { collectionFieldTypes() {
return this.$registry.getAll('collectionField') return this.$registry.getAll('collectionField')
}, },
orientation() {
const device = this.$store.getters['page/getDeviceTypeSelected']
return this.element.orientation[device]
},
}, },
methods: { methods: {

View file

@ -30,10 +30,18 @@
@blur="$v.values.items_per_page.$touch()" @blur="$v.values.items_per_page.$touch()"
></FormInput> ></FormInput>
<FormGroup :label="$t('repeatElementForm.orientationLabel')"> <FormGroup :label="$t('repeatElementForm.orientationLabel')">
<RadioButton v-model="values.orientation" value="vertical"> <RadioButton
v-model="values.orientation"
value="vertical"
icon="iconoir-table-rows"
>
{{ $t('repeatElementForm.orientationVertical') }} {{ $t('repeatElementForm.orientationVertical') }}
</RadioButton> </RadioButton>
<RadioButton v-model="values.orientation" value="horizontal"> <RadioButton
v-model="values.orientation"
value="horizontal"
icon="iconoir-view-columns-3"
>
{{ $t('repeatElementForm.orientationHorizontal') }} {{ $t('repeatElementForm.orientationHorizontal') }}
</RadioButton> </RadioButton>
</FormGroup> </FormGroup>

View file

@ -145,6 +145,30 @@
</template> </template>
<p v-else>{{ $t('tableElementForm.selectSourceFirst') }}</p> <p v-else>{{ $t('tableElementForm.selectSourceFirst') }}</p>
</FormGroup> </FormGroup>
<FormGroup :label="$t('tableElementForm.orientation')">
<DeviceSelector
:device-type-selected="deviceTypeSelected"
direction="row"
@selected="actionSetDeviceTypeSelected"
>
<template #deviceTypeControl="{ deviceType }">
<RadioButton
v-model="values.orientation[deviceType.getType()]"
icon="iconoir-view-columns-3"
:value="TABLE_ORIENTATION.HORIZONTAL"
>
{{ $t('tableElementForm.orientationHorizontal') }}
</RadioButton>
<RadioButton
v-model="values.orientation[deviceType.getType()]"
icon="iconoir-table-rows"
:value="TABLE_ORIENTATION.VERTICAL"
>
{{ $t('tableElementForm.orientationVertical') }}
</RadioButton>
</template>
</DeviceSelector>
</FormGroup>
<ColorInputGroup <ColorInputGroup
v-model="values.button_color" v-model="values.button_color"
:label="$t('tableElementForm.buttonColor')" :label="$t('tableElementForm.buttonColor')"
@ -168,23 +192,38 @@ import {
} from 'vuelidate/lib/validators' } from 'vuelidate/lib/validators'
import elementForm from '@baserow/modules/builder/mixins/elementForm' import elementForm from '@baserow/modules/builder/mixins/elementForm'
import collectionElementForm from '@baserow/modules/builder/mixins/collectionElementForm' import collectionElementForm from '@baserow/modules/builder/mixins/collectionElementForm'
import { TABLE_ORIENTATION } from '@baserow/modules/builder/enums'
import DeviceSelector from '@baserow/modules/builder/components/page/header/DeviceSelector.vue'
import { mapActions, mapGetters } from 'vuex'
export default { export default {
name: 'TableElementForm', name: 'TableElementForm',
components: { ApplicationBuilderFormulaInputGroup }, components: { DeviceSelector, ApplicationBuilderFormulaInputGroup },
mixins: [elementForm, collectionElementForm], mixins: [elementForm, collectionElementForm],
data() { data() {
return { return {
allowedValues: ['data_source_id', 'fields', 'items_per_page'], allowedValues: [
'data_source_id',
'fields',
'items_per_page',
'button_color',
'orientation',
],
values: { values: {
fields: [], fields: [],
data_source_id: null, data_source_id: null,
items_per_page: 1, items_per_page: 1,
button_color: '',
orientation: {},
}, },
userHasChangedDataSource: false, userHasChangedDataSource: false,
} }
}, },
computed: { computed: {
...mapGetters({ deviceTypeSelected: 'page/getDeviceTypeSelected' }),
TABLE_ORIENTATION() {
return TABLE_ORIENTATION
},
orderedCollectionTypes() { orderedCollectionTypes() {
return this.$registry.getOrderedList('collectionField') return this.$registry.getOrderedList('collectionField')
}, },
@ -203,6 +242,9 @@ export default {
}, },
}, },
methods: { methods: {
...mapActions({
actionSetDeviceTypeSelected: 'page/setDeviceTypeSelected',
}),
addField() { addField() {
this.values.fields.push({ this.values.fields.push({
name: getNextAvailableNameInSequence( name: getNextAvailableNameInSequence(

View file

@ -1,23 +1,22 @@
<template> <template>
<ul class="header__filter"> <div class="device-selector">
<li <div
v-for="(deviceType, index) in deviceTypes" v-for="(deviceType, index) in deviceTypes"
:key="deviceType.getType()" :key="index"
class="header__filter-item" class="device-selector__item"
:class="{ 'header__filter-item--no-margin-left': index === 0 }" :class="`device-selector__item--${direction}`"
> >
<a <RadioButton
class="header__filter-link" :key="deviceType.getType()"
:class="{ :value="deviceType.getType()"
'active active--primary': deviceTypeSelected === deviceType.getType(), :icon="deviceType.iconClass"
}" :model-value="deviceTypeSelected"
@click="$emit('selected', deviceType.getType())" class="device-selector__button"
> @click.native="$emit('selected', deviceType.getType())"
<i :class="`header__filter-icon ${deviceType.iconClass}`"></i> ></RadioButton>
</a>
<slot name="deviceTypeControl" :device-type="deviceType"></slot> <slot name="deviceTypeControl" :device-type="deviceType"></slot>
</li> </div>
</ul> </div>
</template> </template>
<script> <script>
@ -28,6 +27,11 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
direction: {
type: String,
required: false,
default: 'column',
},
}, },
computed: { computed: {
deviceTypes() { deviceTypes() {

View file

@ -147,3 +147,8 @@ export const ELEMENT_EVENTS = {
DATA_SOURCE_REMOVED: 'DATA_SOURCE_REMOVED', DATA_SOURCE_REMOVED: 'DATA_SOURCE_REMOVED',
DATA_SOURCE_AFTER_UPDATE: 'DATA_SOURCE_AFTER_UPDATE', DATA_SOURCE_AFTER_UPDATE: 'DATA_SOURCE_AFTER_UPDATE',
} }
export const TABLE_ORIENTATION = {
HORIZONTAL: 'horizontal',
VERTICAL: 'vertical',
}

View file

@ -427,7 +427,10 @@
"itemsPerPagePlaceholder": "Enter value...", "itemsPerPagePlaceholder": "Enter value...",
"selectSourceFirst": "Choose a list data source to begin configuring your fields.", "selectSourceFirst": "Choose a list data source to begin configuring your fields.",
"buttonColor": "Button color", "buttonColor": "Button color",
"refreshFieldsFromDataSource": "refresh fields from data source" "refreshFieldsFromDataSource": "refresh fields from data source",
"orientation": "Orientation",
"orientationHorizontal": "Horizontal",
"orientationVertical": "Vertical"
}, },
"tableElement": { "tableElement": {
"empty": "No items have been found.", "empty": "No items have been found.",
@ -443,7 +446,7 @@
"itemsPerPage": "Items per page", "itemsPerPage": "Items per page",
"itemsPerPagePlaceholder": "Enter value...", "itemsPerPagePlaceholder": "Enter value...",
"itemsPerRowLabel": "Items per row", "itemsPerRowLabel": "Items per row",
"itemsPerRowDescription": "Choose, per device type, what the number of repetitions per row should be.", "itemsPerRowDescription": "Number of columns per row and device type.",
"orientationLabel": "Orientation", "orientationLabel": "Orientation",
"orientationVertical": "Vertical", "orientationVertical": "Vertical",
"orientationHorizontal": "Horizontal" "orientationHorizontal": "Horizontal"

View file

@ -29,3 +29,4 @@
@import 'loading_spinner'; @import 'loading_spinner';
@import 'update_user_source_form'; @import 'update_user_source_form';
@import 'user_source_users_context'; @import 'user_source_users_context';
@import 'device_selector';

View file

@ -0,0 +1,31 @@
.device-selector {
display: flex;
flex-flow: row wrap;
align-items: center;
gap: 5px;
}
.device-selector__item {
display: flex;
gap: 5px;
}
.device-selector__item--column {
flex: 1;
flex-flow: column nowrap;
}
.device-selector__item--row {
flex-direction: row;
flex-grow: 1;
align-items: center;
}
.device-selector__button {
border: none;
box-shadow: none;
.button__icon {
margin: 0 auto;
}
}

View file

@ -1,3 +1,7 @@
.baserow-table-wrapper {
overflow-x: auto;
}
.baserow-table { .baserow-table {
width: 100%; width: 100%;
border: 1px solid $black; border: 1px solid $black;
@ -6,10 +10,6 @@
font-size: 12px; font-size: 12px;
} }
.baserow-table__header-row {
background-color: $palette-neutral-200;
}
.baserow-table__header-cell, .baserow-table__header-cell,
.baserow-table__cell { .baserow-table__cell {
padding: 12px 20px 10px; padding: 12px 20px 10px;
@ -17,13 +17,28 @@
.baserow-table__header-cell { .baserow-table__header-cell {
font-weight: 600; font-weight: 600;
border-bottom: 1px solid $black; background-color: $palette-neutral-200;
} }
.baserow-table__cell { .baserow-table__separator {
border-bottom: 1px solid $black; .baserow-table__row:not(:last-child) & {
border-bottom: 1px solid $black;
}
}
.baserow-table__row:last-child & { .baserow-table--horizontal {
.baserow-table__header-cell,
.baserow-table__cell {
border-bottom: 1px solid $black;
}
.baserow-table__row:last-child .baserow-table__cell {
border-bottom: none; border-bottom: none;
} }
} }
.baserow-table--vertical {
.baserow-table__header-cell {
border-right: 1px solid $black;
}
}

View file

@ -1,14 +1,7 @@
.repeat-element__device-selector { .repeat-element__device-selector {
gap: 10px;
input[type='number'] { input[type='number'] {
margin-top: 5px;
text-align: center; text-align: center;
} }
.header__filter-icon {
margin: 0 auto;
}
} }
.repeat-element__preview { .repeat-element__preview {