mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-15 01:28:30 +00:00
Merge branch '28-change-the-width-of-the-columns-per-grid-view' into 'develop'
Resolve "Change the width of the columns per grid view." Closes #28 See merge request bramw/baserow!39
This commit is contained in:
commit
0d528b8d22
25 changed files with 950 additions and 54 deletions
backend
src/baserow
api/v0
contrib/database
api/v0/views
migrations
views
tests
baserow
api/v0
contrib/database
api/v0/views
view
fixtures
web-frontend/modules
|
@ -153,3 +153,44 @@ def validate_body_custom_fields(registry, base_serializer_class=None,
|
|||
return func(*args, **kwargs)
|
||||
return func_wrapper
|
||||
return validate_decorator
|
||||
|
||||
|
||||
def allowed_includes(*allowed):
|
||||
"""
|
||||
A view method decorator that checks which allowed includes are in the GET
|
||||
parameters of the request. The allowed arguments are going to be added to the
|
||||
view method kwargs and if they are in the includes GET parameter the value will
|
||||
be True.
|
||||
|
||||
Imagine this request:
|
||||
|
||||
# GET /page/?include=cars,unrelated_stuff,bikes
|
||||
@allowed_includes('cars', 'bikes', 'planes')
|
||||
def get(request, cars, bikes, planes):
|
||||
cars >> True
|
||||
bikes >> True
|
||||
planes >> False
|
||||
|
||||
# GET /page/?include=planes
|
||||
@allowed_includes('cars', 'bikes', 'planes')
|
||||
def get(request, cars, bikes, planes):
|
||||
cars >> False
|
||||
bikes >> False
|
||||
planes >> True
|
||||
|
||||
:param allowed: Should have all the allowed include values.
|
||||
:type allowed: list
|
||||
"""
|
||||
|
||||
def validate_decorator(func):
|
||||
def func_wrapper(*args, **kwargs):
|
||||
request = get_request(args)
|
||||
raw_include = request.GET.get('includes', None)
|
||||
includes = raw_include.split(',') if raw_include else []
|
||||
|
||||
for include in allowed:
|
||||
kwargs[include] = include in includes
|
||||
|
||||
return func(*args, **kwargs)
|
||||
return func_wrapper
|
||||
return validate_decorator
|
||||
|
|
|
@ -1,2 +1,11 @@
|
|||
ERROR_GRID_DOES_NOT_EXIST = ('ERROR_GRID_DOES_NOT_EXIST', 404,
|
||||
'The requested grid view does not exist.')
|
||||
ERROR_GRID_DOES_NOT_EXIST = (
|
||||
'ERROR_GRID_DOES_NOT_EXIST',
|
||||
404,
|
||||
'The requested grid view does not exist.'
|
||||
)
|
||||
|
||||
ERROR_UNRELATED_FIELD = (
|
||||
'ERROR_UNRELATED_FIELD',
|
||||
400,
|
||||
'The field is not related to the provided grid view.'
|
||||
)
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from baserow.contrib.database.views.models import GridView, GridViewFieldOptions
|
||||
|
||||
|
||||
class GridViewFieldOptionsField(serializers.Field):
|
||||
default_error_messages = {
|
||||
'invalid_key': _('Field option key must be numeric.'),
|
||||
'invalid_value': _('Must be valid field options.')
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs['source'] = '*'
|
||||
kwargs['read_only'] = False
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
"""
|
||||
This method only validates if the provided data dict is in the correct
|
||||
format. Not if the field id actually exists.
|
||||
|
||||
Example format:
|
||||
{
|
||||
FIELD_ID: {
|
||||
width: 200
|
||||
}
|
||||
}
|
||||
|
||||
:param data: The data that needs to be validated.
|
||||
:type data: dict
|
||||
:return: The validated dict.
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
internal = {}
|
||||
for key, value in data.items():
|
||||
if not (
|
||||
isinstance(key, int) or (isinstance(key, str) and key.isnumeric())
|
||||
):
|
||||
self.fail('invalid_key')
|
||||
serializer = GridViewFieldOptionsSerializer(data=value)
|
||||
if not serializer.is_valid():
|
||||
self.fail('invalid_value')
|
||||
internal[int(key)] = serializer.data
|
||||
return internal
|
||||
|
||||
def to_representation(self, value):
|
||||
"""
|
||||
If the provided value is a GridView instance we need to fetch the options from
|
||||
the database. We can easily use the `get_field_options` of the GridView for
|
||||
that and format the dict the way we want.
|
||||
|
||||
If the provided value is a dict it means the field options have already been
|
||||
provided and validated once, so we can just return that value. The variant is
|
||||
used when we want to validate the input.
|
||||
|
||||
:param value: The prepared value that needs to be serialized.
|
||||
:type value: GridView or dict
|
||||
:return: A dictionary containing the
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
if isinstance(value, GridView):
|
||||
# If the fields are in the context we can pass them into the
|
||||
# `get_field_options` call so that they don't have to be fetched from the
|
||||
# database again.
|
||||
fields = self.context.get('fields')
|
||||
return {
|
||||
field_options.field_id:
|
||||
GridViewFieldOptionsSerializer(field_options).data
|
||||
for field_options in value.get_field_options(True, fields)
|
||||
}
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
class GridViewSerializer(serializers.ModelSerializer):
|
||||
field_options = GridViewFieldOptionsField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = GridView
|
||||
fields = ('field_options',)
|
||||
|
||||
|
||||
class GridViewFieldOptionsSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = GridViewFieldOptions
|
||||
fields = ('width',)
|
|
@ -1,19 +1,23 @@
|
|||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.pagination import LimitOffsetPagination
|
||||
|
||||
from baserow.api.v0.decorators import map_exceptions
|
||||
from baserow.api.v0.decorators import map_exceptions, allowed_includes, validate_body
|
||||
from baserow.api.v0.errors import ERROR_USER_NOT_IN_GROUP
|
||||
from baserow.api.v0.pagination import PageNumberPagination
|
||||
from baserow.core.exceptions import UserNotInGroupError
|
||||
from baserow.contrib.database.api.v0.rows.serializers import (
|
||||
get_row_serializer_class, RowSerializer
|
||||
)
|
||||
from baserow.contrib.database.views.exceptions import ViewDoesNotExist
|
||||
from baserow.contrib.database.api.v0.views.grid.serializers import GridViewSerializer
|
||||
from baserow.contrib.database.views.exceptions import (
|
||||
ViewDoesNotExist, UnrelatedFieldError
|
||||
)
|
||||
from baserow.contrib.database.views.handler import ViewHandler
|
||||
from baserow.contrib.database.views.models import GridView
|
||||
|
||||
from .errors import ERROR_GRID_DOES_NOT_EXIST
|
||||
from .errors import ERROR_GRID_DOES_NOT_EXIST, ERROR_UNRELATED_FIELD
|
||||
|
||||
|
||||
class GridViewView(APIView):
|
||||
|
@ -23,11 +27,15 @@ class GridViewView(APIView):
|
|||
UserNotInGroupError: ERROR_USER_NOT_IN_GROUP,
|
||||
ViewDoesNotExist: ERROR_GRID_DOES_NOT_EXIST
|
||||
})
|
||||
def get(self, request, view_id):
|
||||
@allowed_includes('field_options')
|
||||
def get(self, request, view_id, field_options):
|
||||
"""
|
||||
Lists all the rows of a grid view, paginated either by a page or offset/limit.
|
||||
If the limit get parameter is provided the limit/offset pagination will be used
|
||||
else the page number pagination.
|
||||
|
||||
Optionally the field options can also be included in the response if the the
|
||||
`field_options` are provided in the includes GET parameter.
|
||||
"""
|
||||
|
||||
view = ViewHandler().get_view(request.user, view_id, GridView)
|
||||
|
@ -44,4 +52,38 @@ class GridViewView(APIView):
|
|||
serializer_class = get_row_serializer_class(model, RowSerializer)
|
||||
serializer = serializer_class(page, many=True)
|
||||
|
||||
return paginator.get_paginated_response(serializer.data)
|
||||
response = paginator.get_paginated_response(serializer.data)
|
||||
|
||||
if field_options:
|
||||
# The serializer has the GridViewFieldOptionsField which fetches the
|
||||
# field options from the database and creates them if they don't exist,
|
||||
# but when added to the context the fields don't have to be fetched from
|
||||
# the database again when checking if they exist.
|
||||
context = {'fields': [o['field'] for o in model._field_objects.values()]}
|
||||
response.data.update(**GridViewSerializer(view, context=context).data)
|
||||
|
||||
return response
|
||||
|
||||
@map_exceptions({
|
||||
UserNotInGroupError: ERROR_USER_NOT_IN_GROUP,
|
||||
ViewDoesNotExist: ERROR_GRID_DOES_NOT_EXIST,
|
||||
UnrelatedFieldError: ERROR_UNRELATED_FIELD
|
||||
})
|
||||
@validate_body(GridViewSerializer)
|
||||
def patch(self, request, view_id, data):
|
||||
"""
|
||||
Updates the field options for the provided grid view.
|
||||
|
||||
The following example body data will only update the width of the FIELD_ID
|
||||
and leaves the others untouched.
|
||||
{
|
||||
FIELD_ID: {
|
||||
'width': 200
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
handler = ViewHandler()
|
||||
view = handler.get_view(request.user, view_id, GridView)
|
||||
handler.update_grid_view_field_options(view, data['field_options'])
|
||||
return Response(GridViewSerializer(view).data)
|
||||
|
|
|
@ -42,3 +42,6 @@ class UpdateViewSerializer(serializers.ModelSerializer):
|
|||
class Meta:
|
||||
model = View
|
||||
fields = ('name',)
|
||||
extra_kwargs = {
|
||||
'name': {'required': False}
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@ class ViewsView(APIView):
|
|||
|
||||
table = self.get_table(request.user, table_id)
|
||||
view = ViewHandler().create_view(
|
||||
request.user, table, data['type'], name=data['name'])
|
||||
request.user, table, data.pop('type'), **data)
|
||||
|
||||
serializer = view_type_registry.get_serializer(view, ViewSerializer)
|
||||
return Response(serializer.data)
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
# Generated by Django 2.2.11 on 2020-05-05 12:42
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('database', '0004_auto_20200117_1157'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='GridViewFieldOptions',
|
||||
fields=[
|
||||
('id', models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name='ID'
|
||||
)),
|
||||
('width', models.PositiveIntegerField(default=200)),
|
||||
('field', models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to='database.Field'
|
||||
)),
|
||||
('grid_view', models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to='database.GridView'
|
||||
)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='gridview',
|
||||
name='field_options',
|
||||
field=models.ManyToManyField(
|
||||
through='database.GridViewFieldOptions',
|
||||
to='database.Field'
|
||||
),
|
||||
),
|
||||
]
|
|
@ -7,6 +7,13 @@ class ViewDoesNotExist(Exception):
|
|||
"""Raised when trying to get a view that doesn't exist."""
|
||||
|
||||
|
||||
class UnrelatedFieldError(Exception):
|
||||
"""
|
||||
Raised when a field is not related to the view. For example when someone tries to
|
||||
update field options of a field that does not belong to the view's table.
|
||||
"""
|
||||
|
||||
|
||||
class ViewTypeAlreadyRegistered(InstanceTypeAlreadyRegistered):
|
||||
pass
|
||||
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
from baserow.core.exceptions import UserNotInGroupError
|
||||
from baserow.core.utils import extract_allowed, set_allowed_attrs
|
||||
from baserow.contrib.database.fields.models import Field
|
||||
|
||||
from .exceptions import ViewDoesNotExist
|
||||
from .exceptions import ViewDoesNotExist, UnrelatedFieldError
|
||||
from .registries import view_type_registry
|
||||
from .models import View
|
||||
from .models import View, GridViewFieldOptions
|
||||
|
||||
|
||||
class ViewHandler:
|
||||
|
@ -117,3 +118,30 @@ class ViewHandler:
|
|||
raise UserNotInGroupError(user, group)
|
||||
|
||||
view.delete()
|
||||
|
||||
def update_grid_view_field_options(self, grid_view, field_options, fields=None):
|
||||
"""
|
||||
Updates the field options with the provided values if the field id exists in
|
||||
the table related to the grid view.
|
||||
|
||||
:param grid_view: The grid view for which the field options need to be updated.
|
||||
:type grid_view: Model
|
||||
: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
|
||||
:param fields: Optionally a list of fields can be provided so that they don't
|
||||
have to be fetched again.
|
||||
:type fields: None or list
|
||||
"""
|
||||
|
||||
if not fields:
|
||||
fields = Field.objects.filter(table=grid_view.table)
|
||||
|
||||
allowed_field_ids = [field.id for field in fields]
|
||||
for field_id, options in field_options.items():
|
||||
if int(field_id) not in allowed_field_ids:
|
||||
raise UnrelatedFieldError(f'The field id {field_id} is not related to '
|
||||
f'the grid view.')
|
||||
GridViewFieldOptions.objects.update_or_create(
|
||||
grid_view=grid_view, field_id=field_id, defaults=options
|
||||
)
|
||||
|
|
|
@ -2,6 +2,7 @@ from django.db import models
|
|||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from baserow.core.mixins import OrderableMixin, PolymorphicContentTypeMixin
|
||||
from baserow.contrib.database.fields.models import Field
|
||||
|
||||
|
||||
def get_default_view_content_type():
|
||||
|
@ -29,4 +30,52 @@ class View(OrderableMixin, PolymorphicContentTypeMixin, models.Model):
|
|||
|
||||
|
||||
class GridView(View):
|
||||
pass
|
||||
field_options = models.ManyToManyField(Field, through='GridViewFieldOptions')
|
||||
|
||||
def get_field_options(self, create_if_not_exists=False, fields=None):
|
||||
"""
|
||||
Each field can have unique options per view. This method returns those
|
||||
options per field type and can optionally create the missing ones.
|
||||
|
||||
:param create_if_not_exists: If true the missing GridViewFieldOptions are
|
||||
going to be created. If a fields has been created at a later moment it could
|
||||
be possible that they don't exist yet. If this value is True, the
|
||||
missing relationships are created in that case.
|
||||
:type create_if_not_exists: bool
|
||||
:param fields: If all the fields related to the table of this grid view have
|
||||
already been fetched, they can be provided here to avoid having to fetch
|
||||
them for a second time. This is only needed if `create_if_not_exists` is
|
||||
True.
|
||||
:type fields: list
|
||||
:return: A list of field options instances related to this grid view.
|
||||
:rtype: list
|
||||
"""
|
||||
|
||||
field_options = GridViewFieldOptions.objects.filter(grid_view=self)
|
||||
|
||||
if create_if_not_exists:
|
||||
field_options = list(field_options)
|
||||
if not fields:
|
||||
fields = Field.objects.filter(table=self.table)
|
||||
|
||||
existing_field_ids = [options.field_id for options in field_options]
|
||||
for field in fields:
|
||||
if field.id not in existing_field_ids:
|
||||
field_option = GridViewFieldOptions.objects.create(
|
||||
grid_view=self,
|
||||
field=field
|
||||
)
|
||||
field_options.append(field_option)
|
||||
|
||||
return field_options
|
||||
|
||||
|
||||
class GridViewFieldOptions(models.Model):
|
||||
grid_view = models.ForeignKey(GridView, on_delete=models.CASCADE)
|
||||
field = models.ForeignKey(Field, on_delete=models.CASCADE)
|
||||
# The defaults should be the same as in the `fieldCreated` of the `GridViewType`
|
||||
# abstraction in the web-frontend.
|
||||
width = models.PositiveIntegerField(default=200)
|
||||
|
||||
class Meta:
|
||||
ordering = ('field_id',)
|
||||
|
|
|
@ -10,7 +10,7 @@ from rest_framework.exceptions import APIException
|
|||
from rest_framework.test import APIRequestFactory
|
||||
|
||||
from baserow.api.v0.decorators import (
|
||||
map_exceptions, validate_body, validate_body_custom_fields
|
||||
map_exceptions, validate_body, validate_body_custom_fields, allowed_includes
|
||||
)
|
||||
from baserow.core.models import Group
|
||||
from baserow.core.registry import (
|
||||
|
@ -233,3 +233,40 @@ def test_validate_body_custom_fields():
|
|||
func = MagicMock()
|
||||
|
||||
validate_body_custom_fields(registry)(func)(*[object, request])
|
||||
|
||||
|
||||
def test_allowed_includes():
|
||||
factory = APIRequestFactory()
|
||||
|
||||
request = Request(factory.get(
|
||||
'/some-page/',
|
||||
data={'includes': 'test_1,test_2'},
|
||||
))
|
||||
|
||||
@allowed_includes('test_1', 'test_3')
|
||||
def test_1(self, request, test_1, test_3):
|
||||
assert test_1
|
||||
assert not test_3
|
||||
|
||||
test_1(None, request)
|
||||
|
||||
request = Request(factory.get(
|
||||
'/some-page/',
|
||||
data={'includes': 'test_3'},
|
||||
))
|
||||
|
||||
@allowed_includes('test_1', 'test_3')
|
||||
def test_2(self, request, test_1, test_3):
|
||||
assert not test_1
|
||||
assert test_3
|
||||
|
||||
test_2(None, request)
|
||||
|
||||
request = Request(factory.get('/some-page/',))
|
||||
|
||||
@allowed_includes('test_1', 'test_3')
|
||||
def test_3(self, request, test_1, test_3):
|
||||
assert not test_1
|
||||
assert not test_3
|
||||
|
||||
test_3(None, request)
|
||||
|
|
|
@ -4,7 +4,7 @@ from django.shortcuts import reverse
|
|||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_list_views(api_client, data_fixture):
|
||||
def test_list_rows(api_client, data_fixture):
|
||||
user, token = data_fixture.create_user_and_token(
|
||||
email='test@test.nl', password='password', first_name='Test1')
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
|
@ -92,6 +92,7 @@ def test_list_views(api_client, data_fixture):
|
|||
**{'HTTP_AUTHORIZATION': f'JWT {token}'}
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == 200
|
||||
assert response_json['count'] == 4
|
||||
assert response_json['previous']
|
||||
assert not response_json['next']
|
||||
|
@ -115,9 +116,11 @@ def test_list_views(api_client, data_fixture):
|
|||
**{'HTTP_AUTHORIZATION': f'JWT {token}'}
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == 200
|
||||
assert response_json['count'] == 4
|
||||
assert response_json['results'][0]['id'] == row_1.id
|
||||
assert response_json['results'][1]['id'] == row_2.id
|
||||
assert 'field_options' not in response_json
|
||||
|
||||
url = reverse('api_v0:database:views:grid:list', kwargs={'view_id': grid.id})
|
||||
response = api_client.get(
|
||||
|
@ -126,6 +129,7 @@ def test_list_views(api_client, data_fixture):
|
|||
**{'HTTP_AUTHORIZATION': f'JWT {token}'}
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == 200
|
||||
assert response_json['count'] == 4
|
||||
assert response_json['results'][0]['id'] == row_3.id
|
||||
|
||||
|
@ -145,3 +149,162 @@ def test_list_views(api_client, data_fixture):
|
|||
assert not response_json['previous']
|
||||
assert not response_json['next']
|
||||
assert len(response_json['results']) == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_list_rows_include_field_options(api_client, data_fixture):
|
||||
user, token = data_fixture.create_user_and_token(
|
||||
email='test@test.nl', password='password', first_name='Test1')
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
text_field = data_fixture.create_text_field(table=table, order=0, name='Color',
|
||||
text_default='white')
|
||||
grid = data_fixture.create_grid_view(table=table)
|
||||
|
||||
# The second field is deliberately created after the creation of the grid field
|
||||
# so that the GridViewFieldOptions entry is not created. This should
|
||||
# automatically be created when the page is fetched.
|
||||
number_field = data_fixture.create_number_field(table=table, order=1,
|
||||
name='Horsepower')
|
||||
|
||||
url = reverse('api_v0:database:views:grid:list', kwargs={'view_id': grid.id})
|
||||
response = api_client.get(
|
||||
url,
|
||||
**{'HTTP_AUTHORIZATION': f'JWT {token}'}
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == 200
|
||||
assert 'field_options' not in response_json
|
||||
|
||||
url = reverse('api_v0:database:views:grid:list', kwargs={'view_id': grid.id})
|
||||
response = api_client.get(
|
||||
url,
|
||||
{'includes': 'field_options'},
|
||||
**{'HTTP_AUTHORIZATION': f'JWT {token}'}
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == 200
|
||||
assert len(response_json['field_options']) == 2
|
||||
assert response_json['field_options'][str(text_field.id)]['width'] == 200
|
||||
assert response_json['field_options'][str(number_field.id)]['width'] == 200
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_patch_grid_view(api_client, data_fixture):
|
||||
user, token = data_fixture.create_user_and_token(
|
||||
email='test@test.nl', password='password', first_name='Test1')
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
text_field = data_fixture.create_text_field(table=table)
|
||||
unknown_field = data_fixture.create_text_field()
|
||||
grid = data_fixture.create_grid_view(table=table)
|
||||
# The second field is deliberately created after the creation of the grid field
|
||||
# so that the GridViewFieldOptions entry is not created. This should
|
||||
# automatically be created when the page is fetched.
|
||||
number_field = data_fixture.create_number_field(table=table)
|
||||
grid_2 = data_fixture.create_grid_view()
|
||||
|
||||
url = reverse('api_v0:database:views:grid:list', kwargs={'view_id': grid.id})
|
||||
response = api_client.patch(
|
||||
url,
|
||||
{'field_options': {
|
||||
text_field.id: {'width': 300}
|
||||
}},
|
||||
format='json',
|
||||
HTTP_AUTHORIZATION=f'JWT {token}'
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == 200
|
||||
assert len(response_json['field_options']) == 2
|
||||
assert response_json['field_options'][str(text_field.id)]['width'] == 300
|
||||
assert response_json['field_options'][str(number_field.id)]['width'] == 200
|
||||
options = grid.get_field_options()
|
||||
assert len(options) == 2
|
||||
assert options[0].field_id == text_field.id
|
||||
assert options[0].width == 300
|
||||
assert options[1].field_id == number_field.id
|
||||
assert options[1].width == 200
|
||||
|
||||
url = reverse('api_v0:database:views:grid:list', kwargs={'view_id': grid.id})
|
||||
response = api_client.patch(
|
||||
url,
|
||||
{'field_options': {
|
||||
text_field.id: {'width': 100},
|
||||
number_field.id: {'width': 500}
|
||||
}},
|
||||
format='json',
|
||||
HTTP_AUTHORIZATION=f'JWT {token}'
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == 200
|
||||
assert len(response_json['field_options']) == 2
|
||||
assert response_json['field_options'][str(text_field.id)]['width'] == 100
|
||||
assert response_json['field_options'][str(number_field.id)]['width'] == 500
|
||||
options = grid.get_field_options()
|
||||
assert len(options) == 2
|
||||
assert options[0].field_id == text_field.id
|
||||
assert options[0].width == 100
|
||||
assert options[1].field_id == number_field.id
|
||||
assert options[1].width == 500
|
||||
|
||||
url = reverse('api_v0:database:views:grid:list', kwargs={'view_id': grid.id})
|
||||
response = api_client.patch(
|
||||
url,
|
||||
{},
|
||||
format='json',
|
||||
HTTP_AUTHORIZATION=f'JWT {token}'
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
url = reverse('api_v0:database:views:grid:list', kwargs={'view_id': grid.id})
|
||||
response = api_client.patch(
|
||||
url,
|
||||
{'field_options': {
|
||||
'RANDOM_FIELD': 'TEST'
|
||||
}},
|
||||
format='json',
|
||||
HTTP_AUTHORIZATION=f'JWT {token}'
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == 400
|
||||
assert response_json['error'] == 'ERROR_REQUEST_BODY_VALIDATION'
|
||||
assert response_json['detail']['field_options'][0]['code'] == 'invalid_key'
|
||||
|
||||
url = reverse('api_v0:database:views:grid:list', kwargs={'view_id': grid.id})
|
||||
response = api_client.patch(
|
||||
url,
|
||||
{'field_options': {
|
||||
99999: {'width': 100}
|
||||
}},
|
||||
format='json',
|
||||
HTTP_AUTHORIZATION=f'JWT {token}'
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == 400
|
||||
assert response_json['error'] == 'ERROR_UNRELATED_FIELD'
|
||||
|
||||
url = reverse('api_v0:database:views:grid:list', kwargs={'view_id': grid.id})
|
||||
response = api_client.patch(
|
||||
url,
|
||||
{'field_options': {1: {'width': 'abc'}}},
|
||||
format='json',
|
||||
HTTP_AUTHORIZATION=f'JWT {token}'
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == 400
|
||||
assert response_json['error'] == 'ERROR_REQUEST_BODY_VALIDATION'
|
||||
assert response_json['detail']['field_options'][0]['code'] == 'invalid_value'
|
||||
|
||||
url = reverse('api_v0:database:views:grid:list', kwargs={'view_id': 999})
|
||||
response = api_client.patch(
|
||||
url,
|
||||
**{'HTTP_AUTHORIZATION': f'JWT {token}'}
|
||||
)
|
||||
assert response.status_code == 404
|
||||
assert response.json()['error'] == 'ERROR_GRID_DOES_NOT_EXIST'
|
||||
|
||||
url = reverse('api_v0:database:views:grid:list', kwargs={'view_id': grid_2.id})
|
||||
response = api_client.patch(
|
||||
url,
|
||||
**{'HTTP_AUTHORIZATION': f'JWT {token}'}
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert response.json()['error'] == 'ERROR_USER_NOT_IN_GROUP'
|
||||
|
|
|
@ -182,9 +182,7 @@ def test_update_view(api_client, data_fixture):
|
|||
format='json',
|
||||
HTTP_AUTHORIZATION=f'JWT {token}'
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == 400
|
||||
assert response_json['error'] == 'ERROR_REQUEST_BODY_VALIDATION'
|
||||
assert response.status_code == 200
|
||||
|
||||
url = reverse('api_v0:database:views:item', kwargs={'view_id': view.id})
|
||||
response = api_client.patch(
|
||||
|
|
|
@ -4,7 +4,7 @@ from baserow.core.exceptions import UserNotInGroupError
|
|||
from baserow.contrib.database.views.handler import ViewHandler
|
||||
from baserow.contrib.database.views.models import View, GridView
|
||||
from baserow.contrib.database.views.exceptions import (
|
||||
ViewTypeDoesNotExist, ViewDoesNotExist
|
||||
ViewTypeDoesNotExist, ViewDoesNotExist, UnrelatedFieldError
|
||||
)
|
||||
|
||||
|
||||
|
@ -98,3 +98,54 @@ def test_delete_view(data_fixture):
|
|||
assert View.objects.all().count() == 1
|
||||
handler.delete_view(user=user, view=grid)
|
||||
assert View.objects.all().count() == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_grid_view_field_options(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
grid_view = data_fixture.create_grid_view(table=table)
|
||||
field_1 = data_fixture.create_text_field(table=table)
|
||||
field_2 = data_fixture.create_text_field(table=table)
|
||||
field_3 = data_fixture.create_text_field()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
ViewHandler().update_grid_view_field_options(grid_view=grid_view, field_options={
|
||||
'strange_format': {'height': 150},
|
||||
})
|
||||
|
||||
with pytest.raises(UnrelatedFieldError):
|
||||
ViewHandler().update_grid_view_field_options(grid_view=grid_view, field_options={
|
||||
99999: {'width': 150},
|
||||
})
|
||||
|
||||
with pytest.raises(UnrelatedFieldError):
|
||||
ViewHandler().update_grid_view_field_options(grid_view=grid_view, field_options={
|
||||
field_3.id: {'width': 150},
|
||||
})
|
||||
|
||||
ViewHandler().update_grid_view_field_options(grid_view=grid_view, field_options={
|
||||
str(field_1.id): {'width': 150},
|
||||
field_2.id: {'width': 250}
|
||||
})
|
||||
options_4 = grid_view.get_field_options()
|
||||
|
||||
assert len(options_4) == 2
|
||||
assert options_4[0].width == 150
|
||||
assert options_4[0].field_id == field_1.id
|
||||
assert options_4[1].width == 250
|
||||
assert options_4[1].field_id == field_2.id
|
||||
|
||||
field_4 = data_fixture.create_text_field(table=table)
|
||||
ViewHandler().update_grid_view_field_options(grid_view=grid_view, field_options={
|
||||
field_2.id: {'width': 300},
|
||||
field_4.id: {'width': 50}
|
||||
})
|
||||
options_4 = grid_view.get_field_options()
|
||||
assert len(options_4) == 3
|
||||
assert options_4[0].width == 150
|
||||
assert options_4[0].field_id == field_1.id
|
||||
assert options_4[1].width == 300
|
||||
assert options_4[1].field_id == field_2.id
|
||||
assert options_4[2].width == 50
|
||||
assert options_4[2].field_id == field_4.id
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import pytest
|
||||
|
||||
from django.db import models
|
||||
|
||||
from baserow.contrib.database.views.models import View
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_grid_view_get_field_options(data_fixture):
|
||||
table = data_fixture.create_database_table()
|
||||
table_2 = data_fixture.create_database_table()
|
||||
data_fixture.create_text_field(table=table_2)
|
||||
field_1 = data_fixture.create_text_field(table=table)
|
||||
field_2 = data_fixture.create_text_field(table=table)
|
||||
grid_view = data_fixture.create_grid_view(table=table)
|
||||
|
||||
field_options = grid_view.get_field_options()
|
||||
assert len(field_options) == 2
|
||||
assert field_options[0].field_id == field_1.id
|
||||
assert field_options[0].width == 200
|
||||
assert field_options[1].field_id == field_2.id
|
||||
assert field_options[1].width == 200
|
||||
|
||||
field_3 = data_fixture.create_text_field(table=table)
|
||||
|
||||
field_options = grid_view.get_field_options()
|
||||
assert len(field_options) == 2
|
||||
|
||||
field_options = grid_view.get_field_options(create_if_not_exists=True)
|
||||
assert len(field_options) == 3
|
||||
assert field_options[0].field_id == field_1.id
|
||||
assert field_options[1].field_id == field_2.id
|
||||
assert field_options[2].field_id == field_3.id
|
||||
|
||||
field_options = grid_view.get_field_options(create_if_not_exists=False)
|
||||
assert len(field_options) == 3
|
||||
assert field_options[0].field_id == field_1.id
|
||||
assert field_options[1].field_id == field_2.id
|
||||
assert field_options[2].field_id == field_3.id
|
18
backend/tests/fixtures/view.py
vendored
18
backend/tests/fixtures/view.py
vendored
|
@ -1,4 +1,5 @@
|
|||
from baserow.contrib.database.views.models import GridView
|
||||
from baserow.contrib.database.fields.models import Field
|
||||
from baserow.contrib.database.views.models import GridView, GridViewFieldOptions
|
||||
|
||||
|
||||
class ViewFixtures:
|
||||
|
@ -12,4 +13,17 @@ class ViewFixtures:
|
|||
if 'order' not in kwargs:
|
||||
kwargs['order'] = 0
|
||||
|
||||
return GridView.objects.create(**kwargs)
|
||||
grid_view = GridView.objects.create(**kwargs)
|
||||
self.create_grid_view_field_options(grid_view)
|
||||
return grid_view
|
||||
|
||||
def create_grid_view_field_options(self, grid_view, **kwargs):
|
||||
return [
|
||||
self.create_grid_view_field_option(grid_view, field, **kwargs)
|
||||
for field in Field.objects.filter(table=grid_view.table)
|
||||
]
|
||||
|
||||
def create_grid_view_field_option(self, grid_view, field, **kwargs):
|
||||
return GridViewFieldOptions.objects.create(
|
||||
grid_view=grid_view, field=field, **kwargs
|
||||
)
|
||||
|
|
|
@ -37,6 +37,10 @@
|
|||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.resizing-horizontal {
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0);
|
||||
|
|
|
@ -72,6 +72,7 @@
|
|||
z-index: 3;
|
||||
width: 9px;
|
||||
height: 26px;
|
||||
cursor: col-resize;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
|
@ -323,6 +324,7 @@
|
|||
|
||||
z-index: 2;
|
||||
width: 9px;
|
||||
cursor: col-resize;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
|
|
|
@ -4,29 +4,37 @@
|
|||
ref="scrollbars"
|
||||
horizontal="right"
|
||||
vertical="rightBody"
|
||||
:style="{ left: getLeftWidth() + 'px' }"
|
||||
:style="{ left: widths.left + 'px' }"
|
||||
@vertical="verticalScroll"
|
||||
@horizontal="horizontalScroll"
|
||||
></Scrollbars>
|
||||
<div class="grid-view-left" :style="{ width: getLeftWidth() + 'px' }">
|
||||
<div class="grid-view-inner" :style="{ width: getLeftWidth() + 'px' }">
|
||||
<div class="grid-view-left" :style="{ width: widths.left + 'px' }">
|
||||
<div class="grid-view-inner" :style="{ width: widths.left + 'px' }">
|
||||
<div class="grid-view-head">
|
||||
<div class="grid-view-column" style="width: 60px;"></div>
|
||||
<div
|
||||
class="grid-view-column"
|
||||
:style="{ width: widths.leftReserved + 'px' }"
|
||||
></div>
|
||||
<GridViewFieldType
|
||||
v-if="primary !== null"
|
||||
:field="primary"
|
||||
:style="{ width: widths.fields[primary.id] + 'px' }"
|
||||
></GridViewFieldType>
|
||||
</div>
|
||||
<div ref="leftBody" class="grid-view-body">
|
||||
<div class="grid-view-body-inner">
|
||||
<div
|
||||
class="grid-view-placeholder"
|
||||
style="width: 260px;"
|
||||
:style="{ height: placeholderHeight + 'px' }"
|
||||
:style="{
|
||||
height: placeholderHeight + 'px',
|
||||
width: widths.left + 'px',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="grid-view-placeholder-column"
|
||||
style="width: 260px;"
|
||||
:style="{
|
||||
width: widths.left + 'px',
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
|
@ -40,7 +48,10 @@
|
|||
:class="{ 'grid-view-row-loading': row._.loading }"
|
||||
@contextmenu.prevent="showRowContext($event, row)"
|
||||
>
|
||||
<div class="grid-view-column" style="width: 60px;">
|
||||
<div
|
||||
class="grid-view-column"
|
||||
:style="{ width: widths.leftReserved + 'px' }"
|
||||
>
|
||||
<div class="grid-view-row-info">
|
||||
<div class="grid-view-row-count">{{ row.id }}</div>
|
||||
<a href="#" class="grid-view-row-more">
|
||||
|
@ -54,6 +65,7 @@
|
|||
:field="primary"
|
||||
:row="row"
|
||||
:table="table"
|
||||
:style="{ width: widths.fields[primary.id] + 'px' }"
|
||||
@selected="selectedField(primary, $event.component)"
|
||||
@selectNext="selectNextField(row, primary, fields, primary)"
|
||||
></GridViewField>
|
||||
|
@ -62,7 +74,7 @@
|
|||
<div class="grid-view-row">
|
||||
<div
|
||||
class="grid-view-column"
|
||||
:style="{ width: getLeftWidth() + 'px' }"
|
||||
:style="{ width: widths.left + 'px' }"
|
||||
>
|
||||
<a
|
||||
class="grid-view-add-row"
|
||||
|
@ -78,10 +90,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="grid-view-foot">
|
||||
<div
|
||||
class="grid-view-column"
|
||||
:style="{ width: getLeftWidth() + 'px' }"
|
||||
>
|
||||
<div class="grid-view-column" :style="{ width: widths.left + 'px' }">
|
||||
<div class="grid-view-foot-info">{{ count }} rows</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -90,24 +99,42 @@
|
|||
<div
|
||||
ref="divider"
|
||||
class="grid-view-divider"
|
||||
:style="{ left: getLeftWidth() + 'px' }"
|
||||
:style="{ left: widths.left + 'px' }"
|
||||
></div>
|
||||
<GridViewFieldWidthHandle
|
||||
class="grid-view-divider-width"
|
||||
:style="{ left: widths.left + 'px' }"
|
||||
:grid="view"
|
||||
:field="primary"
|
||||
:width="widths.fields[primary.id]"
|
||||
></GridViewFieldWidthHandle>
|
||||
<div
|
||||
ref="right"
|
||||
class="grid-view-right"
|
||||
:style="{ left: getLeftWidth() + 'px' }"
|
||||
:style="{ left: widths.left + 'px' }"
|
||||
>
|
||||
<div
|
||||
class="grid-view-inner"
|
||||
:style="{ 'min-width': getRightWidth() + 'px' }"
|
||||
:style="{ 'min-width': widths.right + 'px' }"
|
||||
>
|
||||
<div class="grid-view-head">
|
||||
<GridViewFieldType
|
||||
v-for="field in fields"
|
||||
:key="'right-head-field-' + view.id + '-' + field.id"
|
||||
:field="field"
|
||||
></GridViewFieldType>
|
||||
<div class="grid-view-column" style="width: 100px;">
|
||||
:style="{ width: widths.fields[field.id] + 'px' }"
|
||||
>
|
||||
<GridViewFieldWidthHandle
|
||||
class="grid-view-description-width"
|
||||
:grid="view"
|
||||
:field="field"
|
||||
:width="widths.fields[field.id]"
|
||||
></GridViewFieldWidthHandle>
|
||||
</GridViewFieldType>
|
||||
<div
|
||||
class="grid-view-column"
|
||||
:style="{ width: widths.rightAdd + 'px' }"
|
||||
>
|
||||
<a
|
||||
ref="createFieldContextLink"
|
||||
class="grid-view-add-column"
|
||||
|
@ -129,14 +156,14 @@
|
|||
class="grid-view-placeholder"
|
||||
:style="{
|
||||
height: placeholderHeight + 'px',
|
||||
width: fields.length * 200 + 'px',
|
||||
width: widths.rightFieldsOnly + 'px',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-for="(field, index) in fields"
|
||||
:key="'right-placeholder-column-' + view.id + '-' + field.id"
|
||||
v-for="(value, id) in widths.placeholderPositions"
|
||||
:key="'right-placeholder-column-' + view.id + '-' + id"
|
||||
class="grid-view-placeholder-column"
|
||||
:style="{ left: (index + 1) * 200 - 1 + 'px' }"
|
||||
:style="{ left: value - 1 + 'px' }"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
|
@ -160,6 +187,7 @@
|
|||
:field="field"
|
||||
:row="row"
|
||||
:table="table"
|
||||
:style="{ width: widths.fields[field.id] + 'px' }"
|
||||
@selected="selectedField(field, $event.component)"
|
||||
@selectPrevious="
|
||||
selectNextField(row, field, fields, primary, true)
|
||||
|
@ -171,7 +199,7 @@
|
|||
<div class="grid-view-row">
|
||||
<div
|
||||
class="grid-view-column"
|
||||
:style="{ width: getRightWidth(true) + 'px' }"
|
||||
:style="{ width: widths.rightFieldsOnly + 'px' }"
|
||||
>
|
||||
<a
|
||||
class="grid-view-add-row"
|
||||
|
@ -206,7 +234,9 @@ import { mapGetters } from 'vuex'
|
|||
import CreateFieldContext from '@baserow/modules/database/components/field/CreateFieldContext'
|
||||
import GridViewFieldType from '@baserow/modules/database/components/view/grid/GridViewFieldType'
|
||||
import GridViewField from '@baserow/modules/database/components/view/grid/GridViewField'
|
||||
import GridViewFieldWidthHandle from '@baserow/modules/database/components/view/grid/GridViewFieldWidthHandle'
|
||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||
import _ from 'lodash'
|
||||
|
||||
export default {
|
||||
name: 'GridView',
|
||||
|
@ -214,6 +244,7 @@ export default {
|
|||
CreateFieldContext,
|
||||
GridViewFieldType,
|
||||
GridViewField,
|
||||
GridViewFieldWidthHandle,
|
||||
},
|
||||
props: {
|
||||
primary: {
|
||||
|
@ -242,6 +273,9 @@ export default {
|
|||
addHover: false,
|
||||
loading: true,
|
||||
selectedRow: null,
|
||||
widths: {
|
||||
fields: {},
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -251,8 +285,28 @@ export default {
|
|||
rowHeight: 'view/grid/getRowHeight',
|
||||
rowsTop: 'view/grid/getRowsTop',
|
||||
placeholderHeight: 'view/grid/getPlaceholderHeight',
|
||||
fieldOptions: 'view/grid/getAllFieldOptions',
|
||||
}),
|
||||
},
|
||||
watch: {
|
||||
// The field options contain the widths of the field. Every time one of the values
|
||||
// changes we need to recalculate all the widths.
|
||||
fieldOptions: {
|
||||
deep: true,
|
||||
handler(value) {
|
||||
this.calculateWidths(this.primary, this.fields, value)
|
||||
},
|
||||
},
|
||||
// If a field is added or removed we need to recalculate all the widths.
|
||||
fields(value) {
|
||||
this.calculateWidths(this.primary, this.fields, this.fieldOptions)
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// We have to calculate the widths when the component is created so that we can
|
||||
// render the page properly on the server side.
|
||||
this.calculateWidths(this.primary, this.fields, this.fieldOptions)
|
||||
},
|
||||
methods: {
|
||||
scroll(pixelY, pixelX) {
|
||||
const $rightBody = this.$refs.rightBody
|
||||
|
@ -284,16 +338,75 @@ export default {
|
|||
$divider.classList.toggle('shadow', canScroll && left > 0)
|
||||
$right.scrollLeft = left
|
||||
},
|
||||
getLeftWidth() {
|
||||
const space = 60
|
||||
const left = 1 * 200
|
||||
return space + left
|
||||
/**
|
||||
* Calculates the widths of all fields, left side, right side and place holder
|
||||
* positions and returns the values in an object.
|
||||
*/
|
||||
getCalculatedWidths(primary, fields, fieldOptions) {
|
||||
const getFieldWidth = (fieldId) => {
|
||||
return Object.prototype.hasOwnProperty.call(fieldOptions, fieldId)
|
||||
? fieldOptions[fieldId].width
|
||||
: 200
|
||||
}
|
||||
|
||||
// Calculate the widths left side of the grid view. This is the sticky side that
|
||||
// contains the primary field and ids.
|
||||
const leftReserved = 60
|
||||
const leftFieldsOnly = getFieldWidth(primary.id)
|
||||
const left = leftFieldsOnly + leftReserved
|
||||
|
||||
// Calculate the widths of the right side that contains all the other fields.
|
||||
const rightAdd = 100
|
||||
const rightReserved = 100
|
||||
const rightFieldsOnly = fields.reduce(
|
||||
(value, field) => getFieldWidth(field.id) + value,
|
||||
0
|
||||
)
|
||||
const right = rightFieldsOnly + rightAdd + rightReserved
|
||||
|
||||
// Calculate the left positions of the placeholder columns. These are the gray
|
||||
// vertical lines that are always visible, even when the data hasn't loaded yet.
|
||||
let last = 0
|
||||
const placeholderPositions = {}
|
||||
fields.forEach((field) => {
|
||||
last += getFieldWidth(field.id)
|
||||
placeholderPositions[field.id] = last
|
||||
})
|
||||
|
||||
const fieldWidths = {}
|
||||
fieldWidths[primary.id] = getFieldWidth(primary.id)
|
||||
fields.forEach((field) => {
|
||||
fieldWidths[field.id] = getFieldWidth(field.id)
|
||||
})
|
||||
|
||||
return {
|
||||
left,
|
||||
leftReserved,
|
||||
leftFieldsOnly,
|
||||
right,
|
||||
rightReserved,
|
||||
rightAdd,
|
||||
rightFieldsOnly,
|
||||
placeholderPositions,
|
||||
fields: fieldWidths,
|
||||
}
|
||||
},
|
||||
getRightWidth(columnsOnly = false) {
|
||||
const right = this.fields.length * 200
|
||||
const add = 100
|
||||
const space = 100
|
||||
return right + (columnsOnly ? 0 : add + space)
|
||||
/**
|
||||
* This method is called when the fieldOptions or fields changes. The reason why we
|
||||
* don't have smaller methods that are called from the template to calculate the
|
||||
* widths is that because that would quickly result in thousands of function calls
|
||||
* when the smallest things change in the data. This is a speed improving
|
||||
* workaround.
|
||||
*/
|
||||
calculateWidths(primary, fields, fieldOptions) {
|
||||
_.assign(
|
||||
this.widths,
|
||||
this.getCalculatedWidths(primary, fields, fieldOptions)
|
||||
)
|
||||
|
||||
if (this.$refs.scrollbars) {
|
||||
this.$refs.scrollbars.update()
|
||||
}
|
||||
},
|
||||
async addRow() {
|
||||
try {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="grid-view-column" style="width: 200px;" @click="select()">
|
||||
<div class="grid-view-column" @click="select()">
|
||||
<component
|
||||
:is="getFieldComponent(field.type)"
|
||||
ref="column"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="grid-view-column" style="width: 200px;">
|
||||
<div class="grid-view-column">
|
||||
<div
|
||||
class="grid-view-description"
|
||||
:class="{ 'grid-view-description-loading': field._.loading }"
|
||||
|
@ -47,6 +47,7 @@
|
|||
</li>
|
||||
</ul>
|
||||
</Context>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
<template>
|
||||
<div @mousedown="start($event)"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||
|
||||
export default {
|
||||
name: 'GridViewFieldWidthHandle',
|
||||
props: {
|
||||
grid: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
field: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dragging: false,
|
||||
mouseStart: 0,
|
||||
startWidth: 0,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
start(event) {
|
||||
event.preventDefault()
|
||||
this.dragging = true
|
||||
this.mouseStart = event.clientX
|
||||
this.startWidth = parseFloat(this.width)
|
||||
|
||||
this.$el.moveEvent = (event) => this.move(event)
|
||||
this.$el.upEvent = (event) => this.up(event)
|
||||
|
||||
window.addEventListener('mousemove', this.$el.moveEvent)
|
||||
window.addEventListener('mouseup', this.$el.upEvent)
|
||||
document.body.classList.add('resizing-horizontal')
|
||||
},
|
||||
move(event) {
|
||||
event.preventDefault()
|
||||
const difference = event.clientX - this.mouseStart
|
||||
const newWidth = Math.max(this.startWidth + difference, 100)
|
||||
|
||||
this.$store.dispatch('view/grid/setFieldOptionsOfField', {
|
||||
field: this.field,
|
||||
values: { width: newWidth },
|
||||
})
|
||||
},
|
||||
async up(event) {
|
||||
event.preventDefault()
|
||||
const difference = event.clientX - this.mouseStart
|
||||
const newWidth = Math.max(this.startWidth + difference, 100)
|
||||
window.removeEventListener('mousemove', this.$el.moveEvent)
|
||||
window.removeEventListener('mouseup', this.$el.upEvent)
|
||||
document.body.classList.remove('resizing-horizontal')
|
||||
|
||||
if (newWidth === this.startWidth) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('view/grid/updateFieldOptionsOfField', {
|
||||
gridId: this.grid.id,
|
||||
field: this.field,
|
||||
values: { width: newWidth },
|
||||
oldValues: { width: this.startWidth },
|
||||
})
|
||||
} catch (error) {
|
||||
notifyIf(error, 'field')
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -1,12 +1,19 @@
|
|||
import { client } from '@baserow/modules/core/services/client'
|
||||
|
||||
export default {
|
||||
fetchRows({ gridId, limit = 100, offset = null, cancelToken = null }) {
|
||||
fetchRows({
|
||||
gridId,
|
||||
limit = 100,
|
||||
offset = null,
|
||||
cancelToken = null,
|
||||
includeFieldOptions = false,
|
||||
}) {
|
||||
const config = {
|
||||
params: {
|
||||
limit,
|
||||
},
|
||||
}
|
||||
const includes = []
|
||||
|
||||
if (offset !== null) {
|
||||
config.params.offset = offset
|
||||
|
@ -16,6 +23,17 @@ export default {
|
|||
config.cancelToken = cancelToken
|
||||
}
|
||||
|
||||
if (includeFieldOptions) {
|
||||
includes.push('field_options')
|
||||
}
|
||||
|
||||
if (includes.length > 0) {
|
||||
config.params.includes = includes.join(',')
|
||||
}
|
||||
|
||||
return client.get(`/database/views/grid/${gridId}/`, config)
|
||||
},
|
||||
update({ gridId, values }) {
|
||||
return client.patch(`/database/views/grid/${gridId}/`, values)
|
||||
},
|
||||
}
|
||||
|
|
|
@ -12,6 +12,9 @@ export function populateRow(row) {
|
|||
export const state = () => ({
|
||||
loading: false,
|
||||
loaded: false,
|
||||
// Contains the custom field options per view. Things like the field width are
|
||||
// stored here.
|
||||
fieldOptions: {},
|
||||
// Contains the buffered rows that we keep in memory. Depending on the
|
||||
// scrollOffset rows will be added or removed from this buffer. Most of the times,
|
||||
// it will contain 3 times the bufferRequestSize in rows.
|
||||
|
@ -121,6 +124,18 @@ export const mutations = {
|
|||
SET_ROW_LOADING(state, { row, value }) {
|
||||
row._.loading = value
|
||||
},
|
||||
REPLACE_ALL_FIELD_OPTIONS(state, fieldOptions) {
|
||||
state.fieldOptions = fieldOptions
|
||||
},
|
||||
SET_FIELD_OPTIONS_OF_FIELD(state, { fieldId, values }) {
|
||||
if (Object.prototype.hasOwnProperty.call(state.fieldOptions, fieldId)) {
|
||||
_.assign(state.fieldOptions[fieldId], values)
|
||||
} else {
|
||||
state.fieldOptions = _.assign({}, state.fieldOptions, {
|
||||
[fieldId]: values,
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Contains the timeout needed for the delayed delayed scroll top action.
|
||||
|
@ -380,6 +395,7 @@ export const actions = {
|
|||
gridId,
|
||||
offset: 0,
|
||||
limit,
|
||||
includeFieldOptions: true,
|
||||
})
|
||||
data.results.forEach((part, index) => {
|
||||
populateRow(data.results[index])
|
||||
|
@ -399,6 +415,7 @@ export const actions = {
|
|||
endIndex: data.count > 31 ? 31 : data.count,
|
||||
top: 0,
|
||||
})
|
||||
commit('REPLACE_ALL_FIELD_OPTIONS', data.field_options)
|
||||
},
|
||||
/**
|
||||
* Updates a grid view field value. It will immediately be updated in the store
|
||||
|
@ -497,6 +514,41 @@ export const actions = {
|
|||
addField({ commit }, { field, value = null }) {
|
||||
commit('ADD_FIELD', { field, value })
|
||||
},
|
||||
/**
|
||||
* Updates the field options of a given field and also makes an API request to the
|
||||
* backend with the changed values. If the request fails the action is reverted.
|
||||
*/
|
||||
async updateFieldOptionsOfField(
|
||||
{ commit },
|
||||
{ gridId, field, values, oldValues }
|
||||
) {
|
||||
commit('SET_FIELD_OPTIONS_OF_FIELD', {
|
||||
fieldId: field.id,
|
||||
values,
|
||||
})
|
||||
const updateValues = { field_options: {} }
|
||||
updateValues.field_options[field.id] = values
|
||||
|
||||
try {
|
||||
await GridService.update({ gridId, values: updateValues })
|
||||
} catch (error) {
|
||||
commit('SET_FIELD_OPTIONS_OF_FIELD', {
|
||||
fieldId: field.id,
|
||||
values: oldValues,
|
||||
})
|
||||
throw error
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Updates the field options of a given field in the store. So no API request to
|
||||
* the backend is made.
|
||||
*/
|
||||
setFieldOptionsOfField({ commit }, { field, values }) {
|
||||
commit('SET_FIELD_OPTIONS_OF_FIELD', {
|
||||
fieldId: field.id,
|
||||
values,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export const getters = {
|
||||
|
@ -551,6 +603,9 @@ export const getters = {
|
|||
getWindowHeight(state) {
|
||||
return state.windowHeight
|
||||
},
|
||||
getAllFieldOptions(state) {
|
||||
return state.fieldOptions
|
||||
},
|
||||
}
|
||||
|
||||
export default {
|
||||
|
|
|
@ -121,5 +121,15 @@ export class GridViewType extends ViewType {
|
|||
fieldCreated({ dispatch }, table, field, fieldType) {
|
||||
const value = fieldType.getEmptyValue(field)
|
||||
dispatch('view/grid/addField', { field, value }, { root: true })
|
||||
dispatch(
|
||||
'view/grid/setFieldOptionsOfField',
|
||||
{
|
||||
field,
|
||||
// The default values should be the same as in the `GridViewFieldOptions`
|
||||
// model in the backend to stay consistent.
|
||||
values: { width: 200 },
|
||||
},
|
||||
{ root: true }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue