mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-20 03:46:14 +00:00
Resolve "Ordering fields per grid view"
This commit is contained in:
parent
6cbe108f48
commit
9728b64df0
18 changed files with 457 additions and 11 deletions
backend
src/baserow/contrib/database
api/views/grid
migrations
views
tests/baserow/contrib/database
web-frontend/modules
core/assets/scss/components/views
database
|
@ -17,6 +17,14 @@ grid_view_field_options_schema = {
|
||||||
'example': True,
|
'example': True,
|
||||||
'description': 'Whether or not the field should be hidden in the '
|
'description': 'Whether or not the field should be hidden in the '
|
||||||
'current view.'
|
'current view.'
|
||||||
|
},
|
||||||
|
'order': {
|
||||||
|
'type': 'integer',
|
||||||
|
'example': 0,
|
||||||
|
'description': 'The position that the field has within the view, '
|
||||||
|
'lowest first. If there is another field with the '
|
||||||
|
'same order value then the field with the lowest '
|
||||||
|
'id must be shown first.'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -101,7 +101,7 @@ class GridViewSerializer(serializers.ModelSerializer):
|
||||||
class GridViewFieldOptionsSerializer(serializers.ModelSerializer):
|
class GridViewFieldOptionsSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = GridViewFieldOptions
|
model = GridViewFieldOptions
|
||||||
fields = ('width', 'hidden')
|
fields = ('width', 'hidden', 'order')
|
||||||
|
|
||||||
|
|
||||||
class GridViewFilterSerializer(serializers.Serializer):
|
class GridViewFilterSerializer(serializers.Serializer):
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.2.11 on 2021-03-09 18:34
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('database', '0026_auto_20210125_1454'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='gridviewfieldoptions',
|
||||||
|
name='order',
|
||||||
|
field=models.SmallIntegerField(default=32767),
|
||||||
|
),
|
||||||
|
]
|
|
@ -160,7 +160,7 @@ class ViewHandler:
|
||||||
:param user: The user on whose behalf the request is made.
|
:param user: The user on whose behalf the request is made.
|
||||||
:type user: User
|
:type user: User
|
||||||
:param grid_view: The grid view for which the field options need to be updated.
|
:param grid_view: The grid view for which the field options need to be updated.
|
||||||
:type grid_view: Model
|
:type grid_view: GridView
|
||||||
:param field_options: A dict with the field ids as the key and a dict
|
:param field_options: A dict with the field ids as the key and a dict
|
||||||
containing the values that need to be updated as value.
|
containing the values that need to be updated as value.
|
||||||
:type field_options: dict
|
:type field_options: dict
|
||||||
|
|
|
@ -159,6 +159,9 @@ class GridViewFieldOptions(models.Model):
|
||||||
# abstraction in the web-frontend.
|
# abstraction in the web-frontend.
|
||||||
width = models.PositiveIntegerField(default=200)
|
width = models.PositiveIntegerField(default=200)
|
||||||
hidden = models.BooleanField(default=False)
|
hidden = models.BooleanField(default=False)
|
||||||
|
# The default value is the maximum value of the small integer field because a newly
|
||||||
|
# created field must always be last.
|
||||||
|
order = models.SmallIntegerField(default=32767)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('field_id',)
|
ordering = ('field_id',)
|
||||||
|
|
|
@ -226,8 +226,10 @@ def test_list_rows_include_field_options(api_client, data_fixture):
|
||||||
assert len(response_json['field_options']) == 2
|
assert len(response_json['field_options']) == 2
|
||||||
assert response_json['field_options'][str(text_field.id)]['width'] == 200
|
assert response_json['field_options'][str(text_field.id)]['width'] == 200
|
||||||
assert response_json['field_options'][str(text_field.id)]['hidden'] is False
|
assert response_json['field_options'][str(text_field.id)]['hidden'] is False
|
||||||
|
assert response_json['field_options'][str(text_field.id)]['order'] == 32767
|
||||||
assert response_json['field_options'][str(number_field.id)]['width'] == 200
|
assert response_json['field_options'][str(number_field.id)]['width'] == 200
|
||||||
assert response_json['field_options'][str(number_field.id)]['hidden'] is False
|
assert response_json['field_options'][str(number_field.id)]['hidden'] is False
|
||||||
|
assert response_json['field_options'][str(number_field.id)]['order'] == 32767
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
@ -385,16 +387,20 @@ def test_patch_grid_view(api_client, data_fixture):
|
||||||
assert len(response_json['field_options']) == 2
|
assert len(response_json['field_options']) == 2
|
||||||
assert response_json['field_options'][str(text_field.id)]['width'] == 300
|
assert response_json['field_options'][str(text_field.id)]['width'] == 300
|
||||||
assert response_json['field_options'][str(text_field.id)]['hidden'] is True
|
assert response_json['field_options'][str(text_field.id)]['hidden'] is True
|
||||||
|
assert response_json['field_options'][str(text_field.id)]['order'] == 32767
|
||||||
assert response_json['field_options'][str(number_field.id)]['width'] == 200
|
assert response_json['field_options'][str(number_field.id)]['width'] == 200
|
||||||
assert response_json['field_options'][str(number_field.id)]['hidden'] is False
|
assert response_json['field_options'][str(number_field.id)]['hidden'] is False
|
||||||
|
assert response_json['field_options'][str(number_field.id)]['order'] == 32767
|
||||||
options = grid.get_field_options()
|
options = grid.get_field_options()
|
||||||
assert len(options) == 2
|
assert len(options) == 2
|
||||||
assert options[0].field_id == text_field.id
|
assert options[0].field_id == text_field.id
|
||||||
assert options[0].width == 300
|
assert options[0].width == 300
|
||||||
assert options[0].hidden is True
|
assert options[0].hidden is True
|
||||||
|
assert options[0].order == 32767
|
||||||
assert options[1].field_id == number_field.id
|
assert options[1].field_id == number_field.id
|
||||||
assert options[1].width == 200
|
assert options[1].width == 200
|
||||||
assert options[1].hidden is False
|
assert options[1].hidden is False
|
||||||
|
assert options[1].order == 32767
|
||||||
|
|
||||||
url = reverse('api:database:views:grid:list', kwargs={'view_id': grid.id})
|
url = reverse('api:database:views:grid:list', kwargs={'view_id': grid.id})
|
||||||
response = api_client.patch(
|
response = api_client.patch(
|
||||||
|
|
|
@ -14,8 +14,10 @@ def test_grid_view_get_field_options(data_fixture):
|
||||||
assert len(field_options) == 2
|
assert len(field_options) == 2
|
||||||
assert field_options[0].field_id == field_1.id
|
assert field_options[0].field_id == field_1.id
|
||||||
assert field_options[0].width == 200
|
assert field_options[0].width == 200
|
||||||
|
assert field_options[0].order == 32767
|
||||||
assert field_options[1].field_id == field_2.id
|
assert field_options[1].field_id == field_2.id
|
||||||
assert field_options[1].width == 200
|
assert field_options[1].width == 200
|
||||||
|
assert field_options[1].order == 32767
|
||||||
|
|
||||||
field_3 = data_fixture.create_text_field(table=table)
|
field_3 = data_fixture.create_text_field(table=table)
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
* Refactored the GridView component and improved interface speed.
|
* Refactored the GridView component and improved interface speed.
|
||||||
* Prevent websocket reconnect when the connection closes without error.
|
* Prevent websocket reconnect when the connection closes without error.
|
||||||
* Added gunicorn worker test to the CI pipeline.
|
* Added gunicorn worker test to the CI pipeline.
|
||||||
|
* Made it possible to re-order fields in a grid view.
|
||||||
* Show the number of filters and sorts active in the header of a grid view.
|
* Show the number of filters and sorts active in the header of a grid view.
|
||||||
* The first user to sign-up after installation now gets given staff status.
|
* The first user to sign-up after installation now gets given staff status.
|
||||||
|
|
||||||
|
|
|
@ -466,3 +466,17 @@
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grid-view__field-dragging {
|
||||||
|
@include absolute(0, auto);
|
||||||
|
|
||||||
|
z-index: 4;
|
||||||
|
background-color: rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-view__field-target {
|
||||||
|
@include absolute(0, auto, 48px, auto);
|
||||||
|
|
||||||
|
z-index: 5;
|
||||||
|
border-left: solid 1px $color-primary-900;
|
||||||
|
}
|
||||||
|
|
|
@ -53,6 +53,7 @@
|
||||||
:table="table"
|
:table="table"
|
||||||
:view="view"
|
:view="view"
|
||||||
:include-add-field="true"
|
:include-add-field="true"
|
||||||
|
:can-order-fields="true"
|
||||||
:style="{ left: leftWidth + 'px' }"
|
:style="{ left: leftWidth + 'px' }"
|
||||||
@refresh="$emit('refresh', $event)"
|
@refresh="$emit('refresh', $event)"
|
||||||
@row-hover="setRowHover($event.row, $event.value)"
|
@row-hover="setRowHover($event.row, $event.value)"
|
||||||
|
@ -64,6 +65,7 @@
|
||||||
@unselected="unselectedCell($event)"
|
@unselected="unselectedCell($event)"
|
||||||
@select-next="selectNextCell($event)"
|
@select-next="selectNextCell($event)"
|
||||||
@edit-modal="$refs.rowEditModal.show($event.id)"
|
@edit-modal="$refs.rowEditModal.show($event.id)"
|
||||||
|
@scroll="scroll($event.pixelY, $event.pixelX)"
|
||||||
></GridViewSection>
|
></GridViewSection>
|
||||||
<Context ref="rowContext">
|
<Context ref="rowContext">
|
||||||
<ul class="context__menu">
|
<ul class="context__menu">
|
||||||
|
|
|
@ -0,0 +1,259 @@
|
||||||
|
<template>
|
||||||
|
<div v-show="dragging">
|
||||||
|
<div
|
||||||
|
class="grid-view__field-dragging"
|
||||||
|
:style="{ width: draggingWidth + 'px', left: draggingLeft + 'px' }"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="grid-view__field-target"
|
||||||
|
:style="{ left: targetLeft + 'px' }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||||
|
import gridViewHelpers from '@baserow/modules/database/mixins/gridViewHelpers'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'GridViewFieldDragging',
|
||||||
|
mixins: [gridViewHelpers],
|
||||||
|
props: {
|
||||||
|
view: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
fields: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
containerWidth: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// Indicates if the user is dragging a field to another position.
|
||||||
|
dragging: false,
|
||||||
|
// The field object that is being dragged.
|
||||||
|
field: null,
|
||||||
|
// The id of the field where the dragged field must be placed after.
|
||||||
|
targetFieldId: null,
|
||||||
|
// The horizontal starting position of the mouse.
|
||||||
|
mouseStart: 0,
|
||||||
|
// The horizontal scrollbar offset starting position.
|
||||||
|
scrollStart: 0,
|
||||||
|
// The width of the dragging animation, this is equal to the width of the field.
|
||||||
|
draggingWidth: 0,
|
||||||
|
// The position of the dragging animation.
|
||||||
|
draggingLeft: 0,
|
||||||
|
// The position of the target indicator where the field is going to be moved to.
|
||||||
|
targetLeft: 0,
|
||||||
|
// The mouse move event.
|
||||||
|
lastMoveEvent: null,
|
||||||
|
// Indicates if the user is auto scrolling at the moment.
|
||||||
|
autoScrolling: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.cancel()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getFieldLeft(id) {
|
||||||
|
let left = 0
|
||||||
|
for (let i = 0; i < this.fields.length; i++) {
|
||||||
|
if (this.fields[i].id === id) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
left += this.getFieldWidth(this.fields[i].id)
|
||||||
|
}
|
||||||
|
return left
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Called when the field dragging must start. It will register the global mouse
|
||||||
|
* move, mouse up events and keyup events so that the user can drag the field to
|
||||||
|
* the correct position.
|
||||||
|
*/
|
||||||
|
start(field, event) {
|
||||||
|
this.field = field
|
||||||
|
this.targetFieldId = field.id
|
||||||
|
this.dragging = true
|
||||||
|
this.mouseStart = event.clientX
|
||||||
|
this.scrollStart = this.$parent.$el.scrollLeft
|
||||||
|
this.draggingLeft = 0
|
||||||
|
this.targetLeft = 0
|
||||||
|
|
||||||
|
this.$el.moveEvent = (event) => this.move(event)
|
||||||
|
window.addEventListener('mousemove', this.$el.moveEvent)
|
||||||
|
|
||||||
|
this.$el.upEvent = (event) => this.up(event)
|
||||||
|
window.addEventListener('mouseup', this.$el.upEvent)
|
||||||
|
|
||||||
|
this.$el.keydownEvent = (event) => {
|
||||||
|
if (event.keyCode === 27) {
|
||||||
|
// When the user presses the escape key we want to cancel the action
|
||||||
|
this.cancel(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.body.addEventListener('keydown', this.$el.keydownEvent)
|
||||||
|
this.move(event, false)
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* The move method is called when every time the user moves the mouse while
|
||||||
|
* dragging a field. It can also be called while auto scrolling.
|
||||||
|
*/
|
||||||
|
move(event = null, startAutoScroll = true) {
|
||||||
|
if (event !== null) {
|
||||||
|
event.preventDefault()
|
||||||
|
this.lastMoveEvent = event
|
||||||
|
} else {
|
||||||
|
event = this.lastMoveEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the horizontally scrollable element.
|
||||||
|
const element = this.$parent.$el
|
||||||
|
|
||||||
|
this.draggingWidth = this.getFieldWidth(this.field.id)
|
||||||
|
|
||||||
|
// Calculate the left position of the dragging animation. This is the transparent
|
||||||
|
// overlay that has the same width as the field.
|
||||||
|
this.draggingLeft = Math.min(
|
||||||
|
this.getFieldLeft(this.field.id) +
|
||||||
|
event.clientX -
|
||||||
|
this.mouseStart +
|
||||||
|
this.$parent.$el.scrollLeft -
|
||||||
|
this.scrollStart,
|
||||||
|
this.containerWidth - this.draggingWidth
|
||||||
|
)
|
||||||
|
|
||||||
|
// Calculate which after which field we want to place the field that is currently
|
||||||
|
// being dragged. This is named the target. We also calculate what position the
|
||||||
|
// field would have for visualisation purposes.
|
||||||
|
const mouseLeft =
|
||||||
|
event.clientX -
|
||||||
|
element.getBoundingClientRect().left +
|
||||||
|
element.scrollLeft
|
||||||
|
let left = 0
|
||||||
|
for (let i = 0; i < this.fields.length; i++) {
|
||||||
|
const width = this.getFieldWidth(this.fields[i].id)
|
||||||
|
const nextWidth =
|
||||||
|
i + 1 < this.fields.length
|
||||||
|
? this.getFieldWidth(this.fields[i + 1].id)
|
||||||
|
: width
|
||||||
|
const leftHalf = left + Math.floor(width / 2)
|
||||||
|
const rightHalf = left + width + Math.floor(nextWidth / 2)
|
||||||
|
if (i === 0 && mouseLeft < leftHalf) {
|
||||||
|
this.targetFieldId = 0
|
||||||
|
// The value 1 makes sure it is visible instead of falling outside of the
|
||||||
|
// view port.
|
||||||
|
this.targetLeft = 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (mouseLeft > leftHalf && mouseLeft < rightHalf) {
|
||||||
|
this.targetFieldId = this.fields[i].id
|
||||||
|
this.targetLeft = left + width
|
||||||
|
break
|
||||||
|
}
|
||||||
|
left += width
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user is not already auto scrolling, which happens while dragging and
|
||||||
|
// moving the element outside of the view port at the left or right side, we
|
||||||
|
// might need to initiate that process.
|
||||||
|
if (!this.autoScrolling || !startAutoScroll) {
|
||||||
|
const relativeLeft = this.draggingLeft - element.scrollLeft
|
||||||
|
const relativeRight = relativeLeft + this.getFieldWidth(this.field.id)
|
||||||
|
const maxScrollLeft = element.scrollWidth - element.clientWidth
|
||||||
|
let speed = 0
|
||||||
|
|
||||||
|
if (relativeLeft < 0 && element.scrollLeft > 0) {
|
||||||
|
// If the dragging animation falls out of the left side of the viewport we
|
||||||
|
// need to auto scroll to the left.
|
||||||
|
speed = -Math.ceil(Math.min(Math.abs(relativeLeft), 100) / 20)
|
||||||
|
} else if (
|
||||||
|
relativeRight > element.clientWidth &&
|
||||||
|
element.scrollLeft < maxScrollLeft
|
||||||
|
) {
|
||||||
|
// If the dragging animation falls out of the right side of the viewport we
|
||||||
|
// need to auto scroll to the right.
|
||||||
|
speed = Math.ceil(
|
||||||
|
Math.min(relativeRight - element.clientWidth, 100) / 20
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the speed is either a position or negative, so not 0, we know that we
|
||||||
|
// need to start auto scrolling.
|
||||||
|
if (speed !== 0) {
|
||||||
|
this.autoScrolling = true
|
||||||
|
this.$emit('scroll', { pixelY: 0, pixelX: speed })
|
||||||
|
this.$el.scrollTimeout = setTimeout(() => {
|
||||||
|
this.move(null, false)
|
||||||
|
}, 1)
|
||||||
|
} else {
|
||||||
|
this.autoScrolling = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Can be called when the current dragging state needs to be stopped. It will
|
||||||
|
* remove all the created event listeners and timeouts.
|
||||||
|
*/
|
||||||
|
cancel() {
|
||||||
|
this.dragging = false
|
||||||
|
window.removeEventListener('mousemove', this.$el.moveEvent)
|
||||||
|
window.removeEventListener('mouseup', this.$el.upEvent)
|
||||||
|
document.body.addEventListener('keydown', this.$el.keydownEvent)
|
||||||
|
clearTimeout(this.$el.scrollTimeout)
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Called when the user releases the mouse on a the desired position. It will
|
||||||
|
* calculate the new position of the field in the list and if it has changed
|
||||||
|
* position, then the order in the field options is updated accordingly.
|
||||||
|
*/
|
||||||
|
async up(event) {
|
||||||
|
event.preventDefault()
|
||||||
|
this.cancel()
|
||||||
|
|
||||||
|
// We don't need to do anything if the field needs to be placed after itself
|
||||||
|
// because that wouldn't change the position.
|
||||||
|
if (this.field.id === this.targetFieldId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldOrder = this.fields.map((field) => field.id)
|
||||||
|
// Create an array of field ids in the correct order excluding the field that
|
||||||
|
// needs to be repositioned because that one will be added later.
|
||||||
|
const newOrder = this.fields
|
||||||
|
.filter((field) => field.id !== this.field.id)
|
||||||
|
.map((field) => field.id)
|
||||||
|
if (this.targetFieldId === 0) {
|
||||||
|
// If the target field id is 0 the field needs to be moved to the beginning.
|
||||||
|
newOrder.unshift(this.field.id)
|
||||||
|
} else {
|
||||||
|
// Calculate after which field the field that needs to be repositioned needs to
|
||||||
|
// be placed.
|
||||||
|
const targetIndex = newOrder.findIndex(
|
||||||
|
(id) => id === this.targetFieldId
|
||||||
|
)
|
||||||
|
newOrder.splice(targetIndex + 1, 0, this.field.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the new order differs from the old order. If that is not the case we
|
||||||
|
// don't need to update the field options because nothing will be changed.
|
||||||
|
if (JSON.stringify(oldOrder) === JSON.stringify(newOrder)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('view/grid/updateFieldOptionsOrder', {
|
||||||
|
gridId: this.view.id,
|
||||||
|
order: newOrder,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
notifyIf(error, 'view')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -9,6 +9,7 @@
|
||||||
view.sortings.findIndex((sort) => sort.field === field.id) !== -1,
|
view.sortings.findIndex((sort) => sort.field === field.id) !== -1,
|
||||||
}"
|
}"
|
||||||
:style="{ width: width + 'px' }"
|
:style="{ width: width + 'px' }"
|
||||||
|
@mousedown="startDragging($event, field)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="grid-view__description"
|
class="grid-view__description"
|
||||||
|
@ -22,6 +23,7 @@
|
||||||
ref="contextLink"
|
ref="contextLink"
|
||||||
class="grid-view__description-options"
|
class="grid-view__description-options"
|
||||||
@click="$refs.context.toggle($refs.contextLink, 'bottom', 'right', 0)"
|
@click="$refs.context.toggle($refs.contextLink, 'bottom', 'right', 0)"
|
||||||
|
@mousedown.stop
|
||||||
>
|
>
|
||||||
<i class="fas fa-caret-down"></i>
|
<i class="fas fa-caret-down"></i>
|
||||||
</a>
|
</a>
|
||||||
|
@ -132,6 +134,11 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dragging: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
width() {
|
width() {
|
||||||
return this.getFieldWidth(this.field.id)
|
return this.getFieldWidth(this.field.id)
|
||||||
|
@ -210,6 +217,10 @@ export default {
|
||||||
notifyIf(error, 'view')
|
notifyIf(error, 'view')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
startDragging(event, field) {
|
||||||
|
event.preventDefault()
|
||||||
|
this.$emit('dragging', { field, event })
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="{ dragging: dragging }" @mousedown="start($event)"></div>
|
<div :class="{ dragging: dragging }" @mousedown.stop="start($event)"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
:filters="view.filters"
|
:filters="view.filters"
|
||||||
:include-field-width-handles="includeFieldWidthHandles"
|
:include-field-width-handles="includeFieldWidthHandles"
|
||||||
@refresh="$emit('refresh', $event)"
|
@refresh="$emit('refresh', $event)"
|
||||||
|
@dragging="$emit('dragging', $event)"
|
||||||
></GridViewFieldType>
|
></GridViewFieldType>
|
||||||
<div
|
<div
|
||||||
v-if="includeAddField"
|
v-if="includeAddField"
|
||||||
|
|
|
@ -9,6 +9,10 @@
|
||||||
:include-row-details="includeRowDetails"
|
:include-row-details="includeRowDetails"
|
||||||
:include-add-field="includeAddField"
|
:include-add-field="includeAddField"
|
||||||
@refresh="$emit('refresh', $event)"
|
@refresh="$emit('refresh', $event)"
|
||||||
|
@dragging="
|
||||||
|
canOrderFields &&
|
||||||
|
$refs.fieldDragging.start($event.field, $event.event)
|
||||||
|
"
|
||||||
></GridViewHead>
|
></GridViewHead>
|
||||||
<div ref="body" class="grid-view__body">
|
<div ref="body" class="grid-view__body">
|
||||||
<div class="grid-view__body-inner">
|
<div class="grid-view__body-inner">
|
||||||
|
@ -34,6 +38,13 @@
|
||||||
<slot name="foot"></slot>
|
<slot name="foot"></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<GridViewFieldDragging
|
||||||
|
ref="fieldDragging"
|
||||||
|
:view="view"
|
||||||
|
:fields="visibleFields"
|
||||||
|
:container-width="width"
|
||||||
|
@scroll="$emit('scroll', $event)"
|
||||||
|
></GridViewFieldDragging>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -42,7 +53,9 @@ import GridViewHead from '@baserow/modules/database/components/view/grid/GridVie
|
||||||
import GridViewPlaceholder from '@baserow/modules/database/components/view/grid/GridViewPlaceholder'
|
import GridViewPlaceholder from '@baserow/modules/database/components/view/grid/GridViewPlaceholder'
|
||||||
import GridViewRows from '@baserow/modules/database/components/view/grid/GridViewRows'
|
import GridViewRows from '@baserow/modules/database/components/view/grid/GridViewRows'
|
||||||
import GridViewRowAdd from '@baserow/modules/database/components/view/grid/GridViewRowAdd'
|
import GridViewRowAdd from '@baserow/modules/database/components/view/grid/GridViewRowAdd'
|
||||||
|
import GridViewFieldDragging from '@baserow/modules/database/components/view/grid/GridViewFieldDragging'
|
||||||
import gridViewHelpers from '@baserow/modules/database/mixins/gridViewHelpers'
|
import gridViewHelpers from '@baserow/modules/database/mixins/gridViewHelpers'
|
||||||
|
import { GridViewType } from '@baserow/modules/database/viewTypes'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'GridViewSection',
|
name: 'GridViewSection',
|
||||||
|
@ -51,6 +64,7 @@ export default {
|
||||||
GridViewPlaceholder,
|
GridViewPlaceholder,
|
||||||
GridViewRows,
|
GridViewRows,
|
||||||
GridViewRowAdd,
|
GridViewRowAdd,
|
||||||
|
GridViewFieldDragging,
|
||||||
},
|
},
|
||||||
mixins: [gridViewHelpers],
|
mixins: [gridViewHelpers],
|
||||||
props: {
|
props: {
|
||||||
|
@ -81,16 +95,49 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: () => false,
|
default: () => false,
|
||||||
},
|
},
|
||||||
|
canOrderFields: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: () => false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
/**
|
||||||
|
* Returns only the visible fields in the correct order.
|
||||||
|
*/
|
||||||
visibleFields() {
|
visibleFields() {
|
||||||
return this.fields.filter((field) => {
|
return this.fields
|
||||||
const exists = Object.prototype.hasOwnProperty.call(
|
.filter((field) => {
|
||||||
this.fieldOptions,
|
const exists = Object.prototype.hasOwnProperty.call(
|
||||||
field.id
|
this.fieldOptions,
|
||||||
)
|
field.id
|
||||||
return !exists || (exists && !this.fieldOptions[field.id].hidden)
|
)
|
||||||
})
|
return !exists || (exists && !this.fieldOptions[field.id].hidden)
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
const orderA = this.fieldOptions[a.id]
|
||||||
|
? this.fieldOptions[a.id].order
|
||||||
|
: GridViewType.getMaxPossibleOrderValue()
|
||||||
|
const orderB = this.fieldOptions[b.id]
|
||||||
|
? this.fieldOptions[b.id].order
|
||||||
|
: GridViewType.getMaxPossibleOrderValue()
|
||||||
|
|
||||||
|
// First by order.
|
||||||
|
if (orderA > orderB) {
|
||||||
|
return 1
|
||||||
|
} else if (orderA < orderB) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then by id.
|
||||||
|
if (a.id < b.id) {
|
||||||
|
return -1
|
||||||
|
} else if (a.id > b.id) {
|
||||||
|
return 1
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Calculates the total width of the whole section based on the fields and the
|
* Calculates the total width of the whole section based on the fields and the
|
||||||
|
|
|
@ -224,10 +224,20 @@ export const actions = {
|
||||||
/**
|
/**
|
||||||
* Remove the field from the items without calling the server.
|
* Remove the field from the items without calling the server.
|
||||||
*/
|
*/
|
||||||
forceDelete({ commit, dispatch }, field) {
|
async forceDelete(context, field) {
|
||||||
|
const { commit, dispatch } = context
|
||||||
|
|
||||||
// Also delete the related filters if there are any.
|
// Also delete the related filters if there are any.
|
||||||
dispatch('view/fieldDeleted', { field }, { root: true })
|
dispatch('view/fieldDeleted', { field }, { root: true })
|
||||||
commit('DELETE_ITEM', field.id)
|
commit('DELETE_ITEM', field.id)
|
||||||
|
|
||||||
|
// Call the field delete event on all the registered views because they might
|
||||||
|
// need to change things in loaded data. For example the grid field will remove the
|
||||||
|
// field options of that field.
|
||||||
|
const fieldType = this.$registry.get('field', field.type)
|
||||||
|
for (const viewType of Object.values(this.$registry.getAll('view'))) {
|
||||||
|
await viewType.fieldDeleted(context, field, fieldType)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import _ from 'lodash'
|
||||||
import BigNumber from 'bignumber.js'
|
import BigNumber from 'bignumber.js'
|
||||||
|
|
||||||
import { uuid } from '@baserow/modules/core/utils/string'
|
import { uuid } from '@baserow/modules/core/utils/string'
|
||||||
|
import { clone } from '@baserow/modules/core/utils/object'
|
||||||
import GridService from '@baserow/modules/database/services/view/grid'
|
import GridService from '@baserow/modules/database/services/view/grid'
|
||||||
import RowService from '@baserow/modules/database/services/row'
|
import RowService from '@baserow/modules/database/services/row'
|
||||||
import {
|
import {
|
||||||
|
@ -235,6 +236,11 @@ export const mutations = {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
DELETE_FIELD_OPTIONS(state, fieldId) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(state.fieldOptions, fieldId)) {
|
||||||
|
delete state.fieldOptions[fieldId]
|
||||||
|
}
|
||||||
|
},
|
||||||
SET_ROW_HOVER(state, { row, value }) {
|
SET_ROW_HOVER(state, { row, value }) {
|
||||||
row._.hover = value
|
row._.hover = value
|
||||||
},
|
},
|
||||||
|
@ -899,6 +905,47 @@ export const actions = {
|
||||||
forceUpdateAllFieldOptions({ commit }, fieldOptions) {
|
forceUpdateAllFieldOptions({ commit }, fieldOptions) {
|
||||||
commit('UPDATE_ALL_FIELD_OPTIONS', fieldOptions)
|
commit('UPDATE_ALL_FIELD_OPTIONS', fieldOptions)
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Updates the order of all the available field options. The provided order parameter
|
||||||
|
* should be an array containing the field ids in the correct order.
|
||||||
|
*/
|
||||||
|
async updateFieldOptionsOrder(
|
||||||
|
{ commit, getters, dispatch },
|
||||||
|
{ gridId, order }
|
||||||
|
) {
|
||||||
|
const oldFieldOptions = clone(getters.getAllFieldOptions)
|
||||||
|
const newFieldOptions = clone(getters.getAllFieldOptions)
|
||||||
|
|
||||||
|
// Update the order of the field options that have not been provided in the order.
|
||||||
|
// They will get a position that places them after the provided field ids.
|
||||||
|
let i = 0
|
||||||
|
Object.keys(newFieldOptions).forEach((fieldId) => {
|
||||||
|
if (!order.includes(parseInt(fieldId))) {
|
||||||
|
newFieldOptions[fieldId].order = order.length + i
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update create the field options and set the correct order value.
|
||||||
|
order.forEach((fieldId, index) => {
|
||||||
|
const id = fieldId.toString()
|
||||||
|
if (Object.prototype.hasOwnProperty.call(newFieldOptions, id)) {
|
||||||
|
newFieldOptions[fieldId.toString()].order = index
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return await dispatch('updateAllFieldOptions', {
|
||||||
|
gridId,
|
||||||
|
oldFieldOptions,
|
||||||
|
newFieldOptions,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Deletes the field options of the provided field id if they exist.
|
||||||
|
*/
|
||||||
|
forceDeleteFieldOptions({ commit }, fieldId) {
|
||||||
|
commit('DELETE_FIELD_OPTIONS', fieldId)
|
||||||
|
},
|
||||||
setRowHover({ commit }, { row, value }) {
|
setRowHover({ commit }, { row, value }) {
|
||||||
commit('SET_ROW_HOVER', { row, value })
|
commit('SET_ROW_HOVER', { row, value })
|
||||||
},
|
},
|
||||||
|
|
|
@ -112,6 +112,12 @@ export class ViewType extends Registerable {
|
||||||
*/
|
*/
|
||||||
fieldCreated(context, table, field, fieldType) {}
|
fieldCreated(context, table, field, fieldType) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that is called when a field has been deleted. This can be useful to
|
||||||
|
* maintain data integrity.
|
||||||
|
*/
|
||||||
|
fieldDeleted(context, field, fieldType) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method that is called when a field has been changed. This can be useful to
|
* Method that is called when a field has been changed. This can be useful to
|
||||||
* maintain data integrity by updating the values.
|
* maintain data integrity by updating the values.
|
||||||
|
@ -154,6 +160,10 @@ export class ViewType extends Registerable {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GridViewType extends ViewType {
|
export class GridViewType extends ViewType {
|
||||||
|
static getMaxPossibleOrderValue() {
|
||||||
|
return 32767
|
||||||
|
}
|
||||||
|
|
||||||
static getType() {
|
static getType() {
|
||||||
return 'grid'
|
return 'grid'
|
||||||
}
|
}
|
||||||
|
@ -194,12 +204,19 @@ export class GridViewType extends ViewType {
|
||||||
values: {
|
values: {
|
||||||
width: 200,
|
width: 200,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
|
order: GridViewType.getMaxPossibleOrderValue(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ root: true }
|
{ root: true }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fieldDeleted({ dispatch }, field, fieldType) {
|
||||||
|
await dispatch('view/grid/forceDeleteFieldOptions', field.id, {
|
||||||
|
root: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
isCurrentView(store, tableId) {
|
isCurrentView(store, tableId) {
|
||||||
const table = store.getters['table/getSelected']
|
const table = store.getters['table/getSelected']
|
||||||
const grid = store.getters['view/getSelected']
|
const grid = store.getters['view/getSelected']
|
||||||
|
|
Loading…
Add table
Reference in a new issue