From 9728b64df09a8f374104f861affcb1563ac850ce Mon Sep 17 00:00:00 2001 From: Bram Wiepjes <bramw@protonmail.com> Date: Fri, 12 Mar 2021 13:46:22 +0000 Subject: [PATCH] Resolve "Ordering fields per grid view" --- .../database/api/views/grid/schemas.py | 8 + .../database/api/views/grid/serializers.py | 2 +- .../0027_gridviewfieldoptions_order.py | 18 ++ .../baserow/contrib/database/views/handler.py | 2 +- .../baserow/contrib/database/views/models.py | 3 + .../api/views/grid/test_grid_view_views.py | 6 + .../contrib/database/view/test_view_models.py | 2 + changelog.md | 1 + .../assets/scss/components/views/grid.scss | 14 + .../components/view/grid/GridView.vue | 2 + .../view/grid/GridViewFieldDragging.vue | 259 ++++++++++++++++++ .../view/grid/GridViewFieldType.vue | 11 + .../view/grid/GridViewFieldWidthHandle.vue | 2 +- .../components/view/grid/GridViewHead.vue | 1 + .../components/view/grid/GridViewSection.vue | 61 ++++- web-frontend/modules/database/store/field.js | 12 +- .../modules/database/store/view/grid.js | 47 ++++ web-frontend/modules/database/viewTypes.js | 17 ++ 18 files changed, 457 insertions(+), 11 deletions(-) create mode 100644 backend/src/baserow/contrib/database/migrations/0027_gridviewfieldoptions_order.py create mode 100644 web-frontend/modules/database/components/view/grid/GridViewFieldDragging.vue diff --git a/backend/src/baserow/contrib/database/api/views/grid/schemas.py b/backend/src/baserow/contrib/database/api/views/grid/schemas.py index 810bdde8a..a51def7b2 100644 --- a/backend/src/baserow/contrib/database/api/views/grid/schemas.py +++ b/backend/src/baserow/contrib/database/api/views/grid/schemas.py @@ -17,6 +17,14 @@ grid_view_field_options_schema = { 'example': True, 'description': 'Whether or not the field should be hidden in the ' '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.' } } }, diff --git a/backend/src/baserow/contrib/database/api/views/grid/serializers.py b/backend/src/baserow/contrib/database/api/views/grid/serializers.py index 86daf5f66..1bccb767c 100644 --- a/backend/src/baserow/contrib/database/api/views/grid/serializers.py +++ b/backend/src/baserow/contrib/database/api/views/grid/serializers.py @@ -101,7 +101,7 @@ class GridViewSerializer(serializers.ModelSerializer): class GridViewFieldOptionsSerializer(serializers.ModelSerializer): class Meta: model = GridViewFieldOptions - fields = ('width', 'hidden') + fields = ('width', 'hidden', 'order') class GridViewFilterSerializer(serializers.Serializer): diff --git a/backend/src/baserow/contrib/database/migrations/0027_gridviewfieldoptions_order.py b/backend/src/baserow/contrib/database/migrations/0027_gridviewfieldoptions_order.py new file mode 100644 index 000000000..04f711f39 --- /dev/null +++ b/backend/src/baserow/contrib/database/migrations/0027_gridviewfieldoptions_order.py @@ -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), + ), + ] diff --git a/backend/src/baserow/contrib/database/views/handler.py b/backend/src/baserow/contrib/database/views/handler.py index 1267916b3..f0fcd64ce 100644 --- a/backend/src/baserow/contrib/database/views/handler.py +++ b/backend/src/baserow/contrib/database/views/handler.py @@ -160,7 +160,7 @@ class ViewHandler: :param user: The user on whose behalf the request is made. :type user: User :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 containing the values that need to be updated as value. :type field_options: dict diff --git a/backend/src/baserow/contrib/database/views/models.py b/backend/src/baserow/contrib/database/views/models.py index 19a8061a0..36e52ea2a 100644 --- a/backend/src/baserow/contrib/database/views/models.py +++ b/backend/src/baserow/contrib/database/views/models.py @@ -159,6 +159,9 @@ class GridViewFieldOptions(models.Model): # abstraction in the web-frontend. width = models.PositiveIntegerField(default=200) 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: ordering = ('field_id',) diff --git a/backend/tests/baserow/contrib/database/api/views/grid/test_grid_view_views.py b/backend/tests/baserow/contrib/database/api/views/grid/test_grid_view_views.py index 8fdd5638e..18fd57cc8 100644 --- a/backend/tests/baserow/contrib/database/api/views/grid/test_grid_view_views.py +++ b/backend/tests/baserow/contrib/database/api/views/grid/test_grid_view_views.py @@ -226,8 +226,10 @@ def test_list_rows_include_field_options(api_client, data_fixture): 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)]['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)]['hidden'] is False + assert response_json['field_options'][str(number_field.id)]['order'] == 32767 @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 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)]['order'] == 32767 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)]['order'] == 32767 options = grid.get_field_options() assert len(options) == 2 assert options[0].field_id == text_field.id assert options[0].width == 300 assert options[0].hidden is True + assert options[0].order == 32767 assert options[1].field_id == number_field.id assert options[1].width == 200 assert options[1].hidden is False + assert options[1].order == 32767 url = reverse('api:database:views:grid:list', kwargs={'view_id': grid.id}) response = api_client.patch( diff --git a/backend/tests/baserow/contrib/database/view/test_view_models.py b/backend/tests/baserow/contrib/database/view/test_view_models.py index dd4999ae0..14939d062 100644 --- a/backend/tests/baserow/contrib/database/view/test_view_models.py +++ b/backend/tests/baserow/contrib/database/view/test_view_models.py @@ -14,8 +14,10 @@ def test_grid_view_get_field_options(data_fixture): assert len(field_options) == 2 assert field_options[0].field_id == field_1.id 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].width == 200 + assert field_options[1].order == 32767 field_3 = data_fixture.create_text_field(table=table) diff --git a/changelog.md b/changelog.md index c0aaeb876..261b14657 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,7 @@ * Refactored the GridView component and improved interface speed. * Prevent websocket reconnect when the connection closes without error. * 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. * The first user to sign-up after installation now gets given staff status. diff --git a/web-frontend/modules/core/assets/scss/components/views/grid.scss b/web-frontend/modules/core/assets/scss/components/views/grid.scss index 1eaa8504c..fb2c70a43 100644 --- a/web-frontend/modules/core/assets/scss/components/views/grid.scss +++ b/web-frontend/modules/core/assets/scss/components/views/grid.scss @@ -466,3 +466,17 @@ 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; +} diff --git a/web-frontend/modules/database/components/view/grid/GridView.vue b/web-frontend/modules/database/components/view/grid/GridView.vue index 8e93bb690..4b005087d 100644 --- a/web-frontend/modules/database/components/view/grid/GridView.vue +++ b/web-frontend/modules/database/components/view/grid/GridView.vue @@ -53,6 +53,7 @@ :table="table" :view="view" :include-add-field="true" + :can-order-fields="true" :style="{ left: leftWidth + 'px' }" @refresh="$emit('refresh', $event)" @row-hover="setRowHover($event.row, $event.value)" @@ -64,6 +65,7 @@ @unselected="unselectedCell($event)" @select-next="selectNextCell($event)" @edit-modal="$refs.rowEditModal.show($event.id)" + @scroll="scroll($event.pixelY, $event.pixelX)" ></GridViewSection> <Context ref="rowContext"> <ul class="context__menu"> diff --git a/web-frontend/modules/database/components/view/grid/GridViewFieldDragging.vue b/web-frontend/modules/database/components/view/grid/GridViewFieldDragging.vue new file mode 100644 index 000000000..8cb45e6c2 --- /dev/null +++ b/web-frontend/modules/database/components/view/grid/GridViewFieldDragging.vue @@ -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> diff --git a/web-frontend/modules/database/components/view/grid/GridViewFieldType.vue b/web-frontend/modules/database/components/view/grid/GridViewFieldType.vue index 2aa9c42f0..7eef216c8 100644 --- a/web-frontend/modules/database/components/view/grid/GridViewFieldType.vue +++ b/web-frontend/modules/database/components/view/grid/GridViewFieldType.vue @@ -9,6 +9,7 @@ view.sortings.findIndex((sort) => sort.field === field.id) !== -1, }" :style="{ width: width + 'px' }" + @mousedown="startDragging($event, field)" > <div class="grid-view__description" @@ -22,6 +23,7 @@ ref="contextLink" class="grid-view__description-options" @click="$refs.context.toggle($refs.contextLink, 'bottom', 'right', 0)" + @mousedown.stop > <i class="fas fa-caret-down"></i> </a> @@ -132,6 +134,11 @@ export default { required: false, }, }, + data() { + return { + dragging: false, + } + }, computed: { width() { return this.getFieldWidth(this.field.id) @@ -210,6 +217,10 @@ export default { notifyIf(error, 'view') } }, + startDragging(event, field) { + event.preventDefault() + this.$emit('dragging', { field, event }) + }, }, } </script> diff --git a/web-frontend/modules/database/components/view/grid/GridViewFieldWidthHandle.vue b/web-frontend/modules/database/components/view/grid/GridViewFieldWidthHandle.vue index cc45ac0e8..d1da12ffd 100644 --- a/web-frontend/modules/database/components/view/grid/GridViewFieldWidthHandle.vue +++ b/web-frontend/modules/database/components/view/grid/GridViewFieldWidthHandle.vue @@ -1,5 +1,5 @@ <template> - <div :class="{ dragging: dragging }" @mousedown="start($event)"></div> + <div :class="{ dragging: dragging }" @mousedown.stop="start($event)"></div> </template> <script> diff --git a/web-frontend/modules/database/components/view/grid/GridViewHead.vue b/web-frontend/modules/database/components/view/grid/GridViewHead.vue index a3f39f065..e7dc36de3 100644 --- a/web-frontend/modules/database/components/view/grid/GridViewHead.vue +++ b/web-frontend/modules/database/components/view/grid/GridViewHead.vue @@ -14,6 +14,7 @@ :filters="view.filters" :include-field-width-handles="includeFieldWidthHandles" @refresh="$emit('refresh', $event)" + @dragging="$emit('dragging', $event)" ></GridViewFieldType> <div v-if="includeAddField" diff --git a/web-frontend/modules/database/components/view/grid/GridViewSection.vue b/web-frontend/modules/database/components/view/grid/GridViewSection.vue index 9747d7193..c852fb9fc 100644 --- a/web-frontend/modules/database/components/view/grid/GridViewSection.vue +++ b/web-frontend/modules/database/components/view/grid/GridViewSection.vue @@ -9,6 +9,10 @@ :include-row-details="includeRowDetails" :include-add-field="includeAddField" @refresh="$emit('refresh', $event)" + @dragging=" + canOrderFields && + $refs.fieldDragging.start($event.field, $event.event) + " ></GridViewHead> <div ref="body" class="grid-view__body"> <div class="grid-view__body-inner"> @@ -34,6 +38,13 @@ <slot name="foot"></slot> </div> </div> + <GridViewFieldDragging + ref="fieldDragging" + :view="view" + :fields="visibleFields" + :container-width="width" + @scroll="$emit('scroll', $event)" + ></GridViewFieldDragging> </div> </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 GridViewRows from '@baserow/modules/database/components/view/grid/GridViewRows' 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 { GridViewType } from '@baserow/modules/database/viewTypes' export default { name: 'GridViewSection', @@ -51,6 +64,7 @@ export default { GridViewPlaceholder, GridViewRows, GridViewRowAdd, + GridViewFieldDragging, }, mixins: [gridViewHelpers], props: { @@ -81,16 +95,49 @@ export default { required: false, default: () => false, }, + canOrderFields: { + type: Boolean, + required: false, + default: () => false, + }, }, computed: { + /** + * Returns only the visible fields in the correct order. + */ visibleFields() { - return this.fields.filter((field) => { - const exists = Object.prototype.hasOwnProperty.call( - this.fieldOptions, - field.id - ) - return !exists || (exists && !this.fieldOptions[field.id].hidden) - }) + return this.fields + .filter((field) => { + const exists = Object.prototype.hasOwnProperty.call( + this.fieldOptions, + field.id + ) + 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 diff --git a/web-frontend/modules/database/store/field.js b/web-frontend/modules/database/store/field.js index 8f83d7279..f7ccc5daf 100644 --- a/web-frontend/modules/database/store/field.js +++ b/web-frontend/modules/database/store/field.js @@ -224,10 +224,20 @@ export const actions = { /** * 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. dispatch('view/fieldDeleted', { field }, { root: true }) 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) + } }, } diff --git a/web-frontend/modules/database/store/view/grid.js b/web-frontend/modules/database/store/view/grid.js index 9182b7160..a7c2fb189 100644 --- a/web-frontend/modules/database/store/view/grid.js +++ b/web-frontend/modules/database/store/view/grid.js @@ -4,6 +4,7 @@ import _ from 'lodash' import BigNumber from 'bignumber.js' 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 RowService from '@baserow/modules/database/services/row' 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 }) { row._.hover = value }, @@ -899,6 +905,47 @@ export const actions = { forceUpdateAllFieldOptions({ commit }, 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 }) { commit('SET_ROW_HOVER', { row, value }) }, diff --git a/web-frontend/modules/database/viewTypes.js b/web-frontend/modules/database/viewTypes.js index 6a12981b0..0e873f4ae 100644 --- a/web-frontend/modules/database/viewTypes.js +++ b/web-frontend/modules/database/viewTypes.js @@ -112,6 +112,12 @@ export class ViewType extends Registerable { */ 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 * maintain data integrity by updating the values. @@ -154,6 +160,10 @@ export class ViewType extends Registerable { } export class GridViewType extends ViewType { + static getMaxPossibleOrderValue() { + return 32767 + } + static getType() { return 'grid' } @@ -194,12 +204,19 @@ export class GridViewType extends ViewType { values: { width: 200, hidden: false, + order: GridViewType.getMaxPossibleOrderValue(), }, }, { root: true } ) } + async fieldDeleted({ dispatch }, field, fieldType) { + await dispatch('view/grid/forceDeleteFieldOptions', field.id, { + root: true, + }) + } + isCurrentView(store, tableId) { const table = store.getters['table/getSelected'] const grid = store.getters['view/getSelected']