Skip to content

Commit 057f5a1

Browse files
authored
feat: Add drag and drop ordering of sortings and group bys (baserow#5332)
1 parent 161c88e commit 057f5a1

45 files changed

Lines changed: 1875 additions & 199 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/src/baserow/contrib/builder/locale/en/LC_MESSAGES/django.po

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ msgid ""
88
msgstr ""
99
"Project-Id-Version: PACKAGE VERSION\n"
1010
"Report-Msgid-Bugs-To: \n"
11-
"POT-Creation-Date: 2025-11-25 13:02+0000\n"
11+
"POT-Creation-Date: 2026-05-18 18:56+0000\n"
1212
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
1313
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
1414
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -46,18 +46,18 @@ msgstr ""
4646
msgid "Last name"
4747
msgstr ""
4848

49-
#: src/baserow/contrib/builder/data_providers/data_provider_types.py:621
49+
#: src/baserow/contrib/builder/data_providers/data_provider_types.py:636
5050
#, python-format
5151
msgid "%(user_source_name)s member"
5252
msgstr ""
5353

54-
#: src/baserow/contrib/builder/data_sources/service.py:158
54+
#: src/baserow/contrib/builder/data_sources/service.py:165
5555
msgid "Data source"
5656
msgstr ""
5757

58-
#: src/baserow/contrib/builder/elements/mixins.py:612
59-
#: src/baserow/contrib/builder/elements/mixins.py:617
60-
#: src/baserow/contrib/builder/elements/mixins.py:622
58+
#: src/baserow/contrib/builder/elements/mixins.py:643
59+
#: src/baserow/contrib/builder/elements/mixins.py:648
60+
#: src/baserow/contrib/builder/elements/mixins.py:653
6161
#, python-format
6262
msgid "Column %(count)s"
6363
msgstr ""

backend/src/baserow/contrib/database/airtable/registry.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ def get_sorts(
259259
id=sort["id"],
260260
field_id=sort["columnId"],
261261
order=SORT_ORDER_ASC if sort["ascending"] else SORT_ORDER_DESC,
262+
priority=len(view_sorts) + 1,
262263
)
263264
view_sorts.append(view_sort)
264265

@@ -332,6 +333,7 @@ def get_group_bys(
332333
id=group["id"],
333334
field_id=group["columnId"],
334335
order=SORT_ORDER_ASC if ascending else SORT_ORDER_DESC,
336+
priority=len(view_group_by) + 1,
335337
)
336338
view_group_by.append(view_group)
337339

backend/src/baserow/contrib/database/api/views/errors.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@
5959
HTTP_400_BAD_REQUEST,
6060
"The field does not support view sorting on the given type.",
6161
)
62+
ERROR_VIEW_SORT_NOT_IN_VIEW = (
63+
"ERROR_VIEW_SORT_NOT_IN_VIEW",
64+
HTTP_400_BAD_REQUEST,
65+
"The view sort id {e.view_sort_id} does not belong to the view.",
66+
)
6267
ERROR_VIEW_GROUP_BY_DOES_NOT_EXIST = (
6368
"ERROR_VIEW_GROUP_BY_DOES_NOT_EXIST",
6469
HTTP_404_NOT_FOUND,
@@ -79,6 +84,11 @@
7984
HTTP_400_BAD_REQUEST,
8085
"The field does not support view grouping.",
8186
)
87+
ERROR_VIEW_GROUP_BY_NOT_IN_VIEW = (
88+
"ERROR_VIEW_GROUP_BY_NOT_IN_VIEW",
89+
HTTP_400_BAD_REQUEST,
90+
"The view group by id {e.view_group_by_id} does not belong to the view.",
91+
)
8292
ERROR_UNRELATED_FIELD = (
8393
"ERROR_UNRELATED_FIELD",
8494
HTTP_400_BAD_REQUEST,

backend/src/baserow/contrib/database/api/views/serializers.py

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -235,8 +235,11 @@ class Meta:
235235
class ViewSortSerializer(serializers.ModelSerializer):
236236
class Meta:
237237
model = ViewSort
238-
fields = ("id", "view", "field", "order", "type")
239-
extra_kwargs = {"id": {"read_only": True}}
238+
fields = ("id", "view", "field", "order", "type", "priority")
239+
extra_kwargs = {
240+
"id": {"read_only": True},
241+
"priority": {"read_only": True},
242+
}
240243

241244

242245
class CreateViewSortSerializer(serializers.ModelSerializer):
@@ -270,8 +273,12 @@ class Meta:
270273
"order",
271274
"width",
272275
"type",
276+
"priority",
273277
)
274-
extra_kwargs = {"id": {"read_only": True}}
278+
extra_kwargs = {
279+
"id": {"read_only": True},
280+
"priority": {"read_only": True},
281+
}
275282

276283

277284
class CreateViewGroupBySerializer(serializers.ModelSerializer):
@@ -612,6 +619,28 @@ class OrderViewsSerializer(serializers.Serializer):
612619
)
613620

614621

622+
class PrioritizeViewSortingsSerializer(serializers.Serializer):
623+
view_sort_ids = serializers.ListField(
624+
child=serializers.IntegerField(),
625+
help_text=(
626+
"View sort ids in the desired priority order. The sort with the lowest "
627+
"position in the list is applied first."
628+
),
629+
min_length=1,
630+
)
631+
632+
633+
class PrioritizeViewGroupBysSerializer(serializers.Serializer):
634+
view_group_by_ids = serializers.ListField(
635+
child=serializers.IntegerField(),
636+
help_text=(
637+
"View group by ids in the desired priority order. The group by with the "
638+
"lowest position in the list is applied first."
639+
),
640+
min_length=1,
641+
)
642+
643+
615644
class PublicViewAuthRequestSerializer(serializers.Serializer):
616645
password = serializers.CharField()
617646

@@ -625,8 +654,11 @@ class PublicViewSortSerializer(serializers.ModelSerializer):
625654

626655
class Meta:
627656
model = ViewSort
628-
fields = ("id", "view", "field", "order", "type")
629-
extra_kwargs = {"id": {"read_only": True}}
657+
fields = ("id", "view", "field", "order", "type", "priority")
658+
extra_kwargs = {
659+
"id": {"read_only": True},
660+
"priority": {"read_only": True},
661+
}
630662

631663

632664
class PublicViewGroupBySerializer(serializers.ModelSerializer):
@@ -641,8 +673,12 @@ class Meta:
641673
"order",
642674
"width",
643675
"type",
676+
"priority",
644677
)
645-
extra_kwargs = {"id": {"read_only": True}}
678+
extra_kwargs = {
679+
"id": {"read_only": True},
680+
"priority": {"read_only": True},
681+
}
646682

647683

648684
class PublicViewTableSerializer(serializers.Serializer):

backend/src/baserow/contrib/database/api/views/urls.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from .views import (
66
DuplicateViewView,
77
OrderViewsView,
8+
PrioritizeViewGroupBysView,
9+
PrioritizeViewSortingsView,
810
PublicViewAuthView,
911
PublicViewGetRowView,
1012
PublicViewInfoView,
@@ -80,11 +82,21 @@
8082
ViewSortingsView.as_view(),
8183
name="list_sortings",
8284
),
85+
re_path(
86+
r"(?P<view_id>[0-9]+)/sortings/prioritize/$",
87+
PrioritizeViewSortingsView.as_view(),
88+
name="prioritize_sortings",
89+
),
8390
re_path(
8491
r"(?P<view_id>[0-9]+)/group_bys/$",
8592
ViewGroupBysView.as_view(),
8693
name="list_group_bys",
8794
),
95+
re_path(
96+
r"(?P<view_id>[0-9]+)/group_bys/prioritize/$",
97+
PrioritizeViewGroupBysView.as_view(),
98+
name="prioritize_group_bys",
99+
),
88100
re_path(
89101
r"(?P<view_id>[0-9]+)/decorations/$",
90102
ViewDecorationsView.as_view(),

backend/src/baserow/contrib/database/api/views/views.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@
8585
DeleteViewSortActionType,
8686
DuplicateViewActionType,
8787
OrderViewsActionType,
88+
PrioritizeViewGroupBysActionType,
89+
PrioritizeViewSortsActionType,
8890
RotateViewSlugActionType,
8991
UpdateDecorationActionType,
9092
UpdateViewActionType,
@@ -113,13 +115,15 @@
113115
ViewGroupByDoesNotExist,
114116
ViewGroupByFieldAlreadyExist,
115117
ViewGroupByFieldNotSupported,
118+
ViewGroupByNotInView,
116119
ViewGroupByNotSupported,
117120
ViewNotInTable,
118121
ViewOwnershipTypeDoesNotExist,
119122
ViewOwnershipTypeNotCompatibleWithViewType,
120123
ViewSortDoesNotExist,
121124
ViewSortFieldAlreadyExist,
122125
ViewSortFieldNotSupported,
126+
ViewSortNotInView,
123127
ViewSortNotSupported,
124128
)
125129
from baserow.contrib.database.views.handler import ViewHandler
@@ -162,13 +166,15 @@
162166
ERROR_VIEW_GROUP_BY_DOES_NOT_EXIST,
163167
ERROR_VIEW_GROUP_BY_FIELD_ALREADY_EXISTS,
164168
ERROR_VIEW_GROUP_BY_FIELD_NOT_SUPPORTED,
169+
ERROR_VIEW_GROUP_BY_NOT_IN_VIEW,
165170
ERROR_VIEW_GROUP_BY_NOT_SUPPORTED,
166171
ERROR_VIEW_NOT_IN_TABLE,
167172
ERROR_VIEW_OWNERSHIP_TYPE_DOES_NOT_EXIST,
168173
ERROR_VIEW_OWNERSHIP_TYPE_INCOMPATIBLE_WITH_VIEW_TYPE,
169174
ERROR_VIEW_SORT_DOES_NOT_EXIST,
170175
ERROR_VIEW_SORT_FIELD_ALREADY_EXISTS,
171176
ERROR_VIEW_SORT_FIELD_NOT_SUPPORTED,
177+
ERROR_VIEW_SORT_NOT_IN_VIEW,
172178
ERROR_VIEW_SORT_NOT_SUPPORTED,
173179
)
174180
from .serializers import (
@@ -179,6 +185,8 @@
179185
CreateViewSortSerializer,
180186
ListQueryParamatersSerializer,
181187
OrderViewsSerializer,
188+
PrioritizeViewGroupBysSerializer,
189+
PrioritizeViewSortingsSerializer,
182190
PublicViewAuthRequestSerializer,
183191
PublicViewAuthResponseSerializer,
184192
UpdateViewDecorationSerializer,
@@ -1494,6 +1502,55 @@ def delete(self, request, view_decoration_id):
14941502
return Response(status=204)
14951503

14961504

1505+
class PrioritizeViewSortingsView(APIView):
1506+
permission_classes = (IsAuthenticated,)
1507+
1508+
@extend_schema(
1509+
parameters=[
1510+
OpenApiParameter(
1511+
name="view_id",
1512+
location=OpenApiParameter.PATH,
1513+
type=OpenApiTypes.INT,
1514+
description="Updates the priority of the sortings in the view "
1515+
"related to the provided value.",
1516+
),
1517+
CLIENT_SESSION_ID_SCHEMA_PARAMETER,
1518+
CLIENT_UNDO_REDO_ACTION_GROUP_ID_SCHEMA_PARAMETER,
1519+
],
1520+
tags=["Database table view sortings"],
1521+
operation_id="prioritize_database_table_view_sortings",
1522+
description=(
1523+
"Updates the priority of the sorts to match the order of the given "
1524+
"IDs. Sorts earlier in the list are applied first."
1525+
),
1526+
request=PrioritizeViewSortingsSerializer,
1527+
responses={
1528+
204: None,
1529+
400: get_error_schema(
1530+
["ERROR_USER_NOT_IN_GROUP", "ERROR_VIEW_SORT_NOT_IN_VIEW"]
1531+
),
1532+
404: get_error_schema(["ERROR_VIEW_DOES_NOT_EXIST"]),
1533+
},
1534+
)
1535+
@validate_body(PrioritizeViewSortingsSerializer)
1536+
@transaction.atomic
1537+
@map_exceptions(
1538+
{
1539+
ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST,
1540+
UserNotInWorkspace: ERROR_USER_NOT_IN_GROUP,
1541+
ViewSortNotInView: ERROR_VIEW_SORT_NOT_IN_VIEW,
1542+
}
1543+
)
1544+
def post(self, request, data, view_id):
1545+
"""Updates the priority of the sortings in a view."""
1546+
1547+
view = ViewHandler().get_view(view_id)
1548+
action_type_registry.get_by_type(PrioritizeViewSortsActionType).do(
1549+
request.user, view, data["view_sort_ids"]
1550+
)
1551+
return Response(status=204)
1552+
1553+
14971554
class ViewSortingsView(APIView):
14981555
permission_classes = (IsAuthenticated,)
14991556

@@ -2181,6 +2238,56 @@ def get(self, request: Request, slug: str) -> Response:
21812238
)
21822239

21832240

2241+
class PrioritizeViewGroupBysView(APIView):
2242+
permission_classes = (IsAuthenticated,)
2243+
2244+
@extend_schema(
2245+
parameters=[
2246+
OpenApiParameter(
2247+
name="view_id",
2248+
location=OpenApiParameter.PATH,
2249+
type=OpenApiTypes.INT,
2250+
description="Updates the priority of the group bys in the view "
2251+
"related to the provided value.",
2252+
),
2253+
CLIENT_SESSION_ID_SCHEMA_PARAMETER,
2254+
CLIENT_UNDO_REDO_ACTION_GROUP_ID_SCHEMA_PARAMETER,
2255+
],
2256+
tags=["Database table view groupings"],
2257+
operation_id="prioritize_database_table_view_group_bys",
2258+
description=(
2259+
"Updates the priority of the provided view group by ids to the matching "
2260+
"position that the id has in the list. The group by with the lowest "
2261+
"position in the list is applied first when ordering rows."
2262+
),
2263+
request=PrioritizeViewGroupBysSerializer,
2264+
responses={
2265+
204: None,
2266+
400: get_error_schema(
2267+
["ERROR_USER_NOT_IN_GROUP", "ERROR_VIEW_GROUP_BY_NOT_IN_VIEW"]
2268+
),
2269+
404: get_error_schema(["ERROR_VIEW_DOES_NOT_EXIST"]),
2270+
},
2271+
)
2272+
@validate_body(PrioritizeViewGroupBysSerializer)
2273+
@transaction.atomic
2274+
@map_exceptions(
2275+
{
2276+
ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST,
2277+
UserNotInWorkspace: ERROR_USER_NOT_IN_GROUP,
2278+
ViewGroupByNotInView: ERROR_VIEW_GROUP_BY_NOT_IN_VIEW,
2279+
}
2280+
)
2281+
def post(self, request, data, view_id):
2282+
"""Updates the priority of the group bys in a view."""
2283+
2284+
view = ViewHandler().get_view(view_id)
2285+
action_type_registry.get_by_type(PrioritizeViewGroupBysActionType).do(
2286+
request.user, view, data["view_group_by_ids"]
2287+
)
2288+
return Response(status=204)
2289+
2290+
21842291
class ViewGroupBysView(APIView):
21852292
permission_classes = (IsAuthenticated,)
21862293

backend/src/baserow/contrib/database/apps.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ def ready(self):
115115
DuplicateViewActionType,
116116
EditFormRowActionType,
117117
OrderViewsActionType,
118+
PrioritizeViewGroupBysActionType,
119+
PrioritizeViewSortsActionType,
118120
RotateViewSlugActionType,
119121
SubmitFormActionType,
120122
UpdateDecorationActionType,
@@ -138,9 +140,11 @@ def ready(self):
138140
action_type_registry.register(CreateViewSortActionType())
139141
action_type_registry.register(UpdateViewSortActionType())
140142
action_type_registry.register(DeleteViewSortActionType())
143+
action_type_registry.register(PrioritizeViewSortsActionType())
141144
action_type_registry.register(CreateViewGroupByActionType())
142145
action_type_registry.register(UpdateViewGroupByActionType())
143146
action_type_registry.register(DeleteViewGroupByActionType())
147+
action_type_registry.register(PrioritizeViewGroupBysActionType())
144148
action_type_registry.register(SubmitFormActionType())
145149
action_type_registry.register(EditFormRowActionType())
146150
action_type_registry.register(RotateViewSlugActionType())
@@ -861,6 +865,8 @@ def ready(self):
861865
ListViewsOperationType,
862866
ListViewSortOperationType,
863867
OrderViewsOperationType,
868+
PrioritizeViewGroupByOperationType,
869+
PrioritizeViewSortOperationType,
864870
ReadAdjacentViewRowOperationType,
865871
ReadAggregationsViewOperationType,
866872
ReadViewDecorationOperationType,
@@ -939,6 +945,8 @@ def ready(self):
939945
operation_type_registry.register(SubmitAnonymousFieldValuesOperationType())
940946
operation_type_registry.register(DeleteViewSortOperationType())
941947
operation_type_registry.register(DeleteViewGroupByOperationType())
948+
operation_type_registry.register(PrioritizeViewSortOperationType())
949+
operation_type_registry.register(PrioritizeViewGroupByOperationType())
942950
operation_type_registry.register(UpdateViewSlugOperationType())
943951
operation_type_registry.register(UpdateViewPublicOperationType())
944952
operation_type_registry.register(ReadViewsOrderOperationType())

0 commit comments

Comments
 (0)