mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-11 07:51:20 +00:00
Resolve "Make the applications orderable"
This commit is contained in:
parent
7e1fd1c184
commit
7d4aeeec36
21 changed files with 397 additions and 40 deletions
backend
src/baserow
api/applications
core
ws
tests/baserow
web-frontend/modules
core
assets/scss/components
components
plugins
services
store
database/components/sidebar
|
@ -1,4 +1,4 @@
|
|||
from rest_framework.status import HTTP_404_NOT_FOUND
|
||||
from rest_framework.status import HTTP_400_BAD_REQUEST, HTTP_404_NOT_FOUND
|
||||
|
||||
|
||||
ERROR_APPLICATION_DOES_NOT_EXIST = (
|
||||
|
@ -6,3 +6,8 @@ ERROR_APPLICATION_DOES_NOT_EXIST = (
|
|||
HTTP_404_NOT_FOUND,
|
||||
"The requested application does not exist.",
|
||||
)
|
||||
ERROR_APPLICATION_NOT_IN_GROUP = (
|
||||
"ERROR_APPLICATION_NOT_IN_GROUP",
|
||||
HTTP_400_BAD_REQUEST,
|
||||
"The application id {e.application_id} does not belong to the group.",
|
||||
)
|
||||
|
|
|
@ -48,6 +48,13 @@ class ApplicationUpdateSerializer(serializers.ModelSerializer):
|
|||
fields = ("name",)
|
||||
|
||||
|
||||
class OrderApplicationsSerializer(serializers.Serializer):
|
||||
application_ids = serializers.ListField(
|
||||
child=serializers.IntegerField(),
|
||||
help_text="Application ids in the desired order.",
|
||||
)
|
||||
|
||||
|
||||
def get_application_serializer(instance, **kwargs):
|
||||
"""
|
||||
Returns an instantiated serializer based on the instance class type. Custom
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
from django.conf.urls import url
|
||||
|
||||
from .views import ApplicationsView, AllApplicationsView, ApplicationView
|
||||
from .views import (
|
||||
ApplicationsView,
|
||||
AllApplicationsView,
|
||||
ApplicationView,
|
||||
OrderApplicationsView,
|
||||
)
|
||||
|
||||
|
||||
app_name = "baserow.api.group"
|
||||
|
@ -8,6 +13,11 @@ app_name = "baserow.api.group"
|
|||
|
||||
urlpatterns = [
|
||||
url(r"group/(?P<group_id>[0-9]+)/$", ApplicationsView.as_view(), name="list"),
|
||||
url(
|
||||
r"group/(?P<group_id>[0-9]+)/order/$",
|
||||
OrderApplicationsView.as_view(),
|
||||
name="order",
|
||||
),
|
||||
url(r"(?P<application_id>[0-9]+)/$", ApplicationView.as_view(), name="item"),
|
||||
url(r"$", AllApplicationsView.as_view(), name="list"),
|
||||
]
|
||||
|
|
|
@ -11,13 +11,17 @@ from baserow.api.decorators import validate_body, map_exceptions
|
|||
from baserow.api.errors import ERROR_USER_NOT_IN_GROUP, ERROR_GROUP_DOES_NOT_EXIST
|
||||
from baserow.api.schemas import get_error_schema
|
||||
from baserow.api.utils import PolymorphicMappingSerializer
|
||||
from baserow.api.applications.errors import ERROR_APPLICATION_DOES_NOT_EXIST
|
||||
from baserow.api.applications.errors import (
|
||||
ERROR_APPLICATION_DOES_NOT_EXIST,
|
||||
ERROR_APPLICATION_NOT_IN_GROUP,
|
||||
)
|
||||
from baserow.core.models import Application
|
||||
from baserow.core.handler import CoreHandler
|
||||
from baserow.core.exceptions import (
|
||||
UserNotInGroup,
|
||||
GroupDoesNotExist,
|
||||
ApplicationDoesNotExist,
|
||||
ApplicationNotInGroup,
|
||||
)
|
||||
from baserow.core.registries import application_type_registry
|
||||
|
||||
|
@ -25,6 +29,7 @@ from .serializers import (
|
|||
ApplicationSerializer,
|
||||
ApplicationCreateSerializer,
|
||||
ApplicationUpdateSerializer,
|
||||
OrderApplicationsSerializer,
|
||||
get_application_serializer,
|
||||
)
|
||||
|
||||
|
@ -312,3 +317,50 @@ class ApplicationView(APIView):
|
|||
CoreHandler().delete_application(request.user, application)
|
||||
|
||||
return Response(status=204)
|
||||
|
||||
|
||||
class OrderApplicationsView(APIView):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="group_id",
|
||||
location=OpenApiParameter.PATH,
|
||||
type=OpenApiTypes.INT,
|
||||
description="Updates the order of the applications in the group "
|
||||
"related to the provided value.",
|
||||
),
|
||||
],
|
||||
tags=["Applications"],
|
||||
operation_id="order_applications",
|
||||
description=(
|
||||
"Changes the order of the provided application ids to the matching "
|
||||
"position that the id has in the list. If the authorized user does not "
|
||||
"belong to the group it will be ignored. The order of the not provided "
|
||||
"tables will be set to `0`."
|
||||
),
|
||||
request=OrderApplicationsSerializer,
|
||||
responses={
|
||||
204: None,
|
||||
400: get_error_schema(
|
||||
["ERROR_USER_NOT_IN_GROUP", "ERROR_APPLICATION_NOT_IN_GROUP"]
|
||||
),
|
||||
404: get_error_schema(["ERROR_GROUP_DOES_NOT_EXIST"]),
|
||||
},
|
||||
)
|
||||
@validate_body(OrderApplicationsSerializer)
|
||||
@transaction.atomic
|
||||
@map_exceptions(
|
||||
{
|
||||
GroupDoesNotExist: ERROR_GROUP_DOES_NOT_EXIST,
|
||||
UserNotInGroup: ERROR_USER_NOT_IN_GROUP,
|
||||
ApplicationNotInGroup: ERROR_APPLICATION_NOT_IN_GROUP,
|
||||
}
|
||||
)
|
||||
def post(self, request, data, group_id):
|
||||
"""Updates to order of the applications in a table."""
|
||||
|
||||
group = CoreHandler().get_group(group_id)
|
||||
CoreHandler().order_applications(request.user, group, data["application_ids"])
|
||||
return Response(status=204)
|
||||
|
|
|
@ -51,6 +51,18 @@ class ApplicationDoesNotExist(Exception):
|
|||
"""Raised when trying to get an application that does not exist."""
|
||||
|
||||
|
||||
class ApplicationNotInGroup(Exception):
|
||||
"""Raised when a provided application does not belong to a group."""
|
||||
|
||||
def __init__(self, application_id=None, *args, **kwargs):
|
||||
self.application_id = application_id
|
||||
super().__init__(
|
||||
f"The application {application_id} does not belong to the group.",
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class InstanceTypeAlreadyRegistered(Exception):
|
||||
"""
|
||||
Raised when the instance model instance is already registered in the registry.
|
||||
|
|
|
@ -28,6 +28,7 @@ from .models import (
|
|||
from .exceptions import (
|
||||
GroupDoesNotExist,
|
||||
ApplicationDoesNotExist,
|
||||
ApplicationNotInGroup,
|
||||
BaseURLHostnameNotAllowed,
|
||||
GroupInvitationEmailMismatch,
|
||||
GroupInvitationDoesNotExist,
|
||||
|
@ -43,6 +44,7 @@ from .signals import (
|
|||
application_created,
|
||||
application_updated,
|
||||
application_deleted,
|
||||
applications_reordered,
|
||||
group_created,
|
||||
group_updated,
|
||||
group_deleted,
|
||||
|
@ -656,6 +658,33 @@ class CoreHandler:
|
|||
|
||||
return application
|
||||
|
||||
def order_applications(self, user, group, order):
|
||||
"""
|
||||
Updates the order of the applications in the given group. The order of the
|
||||
applications that are not in the `order` parameter set set to `0`.
|
||||
|
||||
:param user: The user on whose behalf the tables are ordered.
|
||||
:type user: User
|
||||
:param group: The group of which the applications must be updated.
|
||||
:type group: Group
|
||||
:param order: A list containing the application ids in the desired order.
|
||||
:type order: list
|
||||
:raises ApplicationNotInGroup: If one of the applications ids in the order does
|
||||
not belong to the database.
|
||||
"""
|
||||
|
||||
group.has_user(user, raise_error=True)
|
||||
|
||||
queryset = Application.objects.filter(group_id=group.id)
|
||||
application_ids = queryset.values_list("id", flat=True)
|
||||
|
||||
for application_id in order:
|
||||
if application_id not in application_ids:
|
||||
raise ApplicationNotInGroup(application_id)
|
||||
|
||||
Application.order_objects(queryset, order)
|
||||
applications_reordered.send(self, group=group, order=order, user=user)
|
||||
|
||||
def delete_application(self, user, application):
|
||||
"""
|
||||
Deletes an existing application instance if the user has access to the
|
||||
|
|
|
@ -11,3 +11,4 @@ group_user_deleted = Signal()
|
|||
application_created = Signal()
|
||||
application_updated = Signal()
|
||||
application_deleted = Signal()
|
||||
applications_reordered = Signal()
|
||||
|
|
|
@ -109,3 +109,18 @@ def application_deleted(sender, application_id, application, user, **kwargs):
|
|||
getattr(user, "web_socket_id", None),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@receiver(signals.applications_reordered)
|
||||
def applications_reordered(sender, group, order, user, **kwargs):
|
||||
transaction.on_commit(
|
||||
lambda: broadcast_to_group.delay(
|
||||
group.id,
|
||||
{
|
||||
"type": "applications_reordered",
|
||||
"group_id": group.id,
|
||||
"order": order,
|
||||
},
|
||||
getattr(user, "web_socket_id", None),
|
||||
)
|
||||
)
|
||||
|
|
|
@ -2,6 +2,7 @@ import pytest
|
|||
|
||||
from rest_framework.status import (
|
||||
HTTP_200_OK,
|
||||
HTTP_204_NO_CONTENT,
|
||||
HTTP_400_BAD_REQUEST,
|
||||
HTTP_401_UNAUTHORIZED,
|
||||
HTTP_404_NOT_FOUND,
|
||||
|
@ -235,3 +236,66 @@ def test_delete_application(api_client, data_fixture):
|
|||
assert response.status_code == 204
|
||||
|
||||
assert Database.objects.all().count() == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_tables(api_client, data_fixture):
|
||||
user, token = data_fixture.create_user_and_token(
|
||||
email="test@test.nl", password="password", first_name="Test1"
|
||||
)
|
||||
group_1 = data_fixture.create_group(user=user)
|
||||
group_2 = data_fixture.create_group()
|
||||
application_1 = data_fixture.create_database_application(group=group_1, order=1)
|
||||
application_2 = data_fixture.create_database_application(group=group_1, order=2)
|
||||
application_3 = data_fixture.create_database_application(group=group_1, order=3)
|
||||
|
||||
response = api_client.post(
|
||||
reverse("api:applications:order", kwargs={"group_id": group_2.id}),
|
||||
{"application_ids": []},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response.json()["error"] == "ERROR_USER_NOT_IN_GROUP"
|
||||
|
||||
response = api_client.post(
|
||||
reverse("api:applications:order", kwargs={"group_id": 999999}),
|
||||
{"application_ids": []},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_404_NOT_FOUND
|
||||
assert response.json()["error"] == "ERROR_GROUP_DOES_NOT_EXIST"
|
||||
|
||||
response = api_client.post(
|
||||
reverse("api:applications:order", kwargs={"group_id": group_1.id}),
|
||||
{"application_ids": [0]},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response.json()["error"] == "ERROR_APPLICATION_NOT_IN_GROUP"
|
||||
|
||||
response = api_client.post(
|
||||
reverse("api:applications:order", kwargs={"group_id": group_1.id}),
|
||||
{"application_ids": ["test"]},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response.json()["error"] == "ERROR_REQUEST_BODY_VALIDATION"
|
||||
|
||||
response = api_client.post(
|
||||
reverse("api:applications:order", kwargs={"group_id": group_1.id}),
|
||||
{"application_ids": [application_3.id, application_2.id, application_1.id]},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_204_NO_CONTENT
|
||||
|
||||
application_1.refresh_from_db()
|
||||
application_2.refresh_from_db()
|
||||
application_3.refresh_from_db()
|
||||
assert application_1.order == 3
|
||||
assert application_2.order == 2
|
||||
assert application_3.order == 1
|
||||
|
|
|
@ -14,6 +14,7 @@ from baserow.contrib.database.models import Database, Table
|
|||
from baserow.core.exceptions import (
|
||||
UserNotInGroup,
|
||||
ApplicationTypeDoesNotExist,
|
||||
ApplicationNotInGroup,
|
||||
GroupDoesNotExist,
|
||||
GroupUserDoesNotExist,
|
||||
ApplicationDoesNotExist,
|
||||
|
@ -694,6 +695,66 @@ def test_update_database_application(send_mock, data_fixture):
|
|||
assert database.name == "Test 1"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@patch("baserow.core.signals.applications_reordered.send")
|
||||
def test_order_applications(send_mock, data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
user_2 = data_fixture.create_user()
|
||||
group = data_fixture.create_group(user=user)
|
||||
application_1 = data_fixture.create_database_application(group=group, order=1)
|
||||
application_2 = data_fixture.create_database_application(group=group, order=2)
|
||||
application_3 = data_fixture.create_database_application(group=group, order=3)
|
||||
|
||||
handler = CoreHandler()
|
||||
|
||||
with pytest.raises(UserNotInGroup):
|
||||
handler.order_applications(user=user_2, group=group, order=[])
|
||||
|
||||
with pytest.raises(ApplicationNotInGroup):
|
||||
handler.order_applications(user=user, group=group, order=[0])
|
||||
|
||||
handler.order_applications(
|
||||
user=user,
|
||||
group=group,
|
||||
order=[application_3.id, application_2.id, application_1.id],
|
||||
)
|
||||
application_1.refresh_from_db()
|
||||
application_2.refresh_from_db()
|
||||
application_3.refresh_from_db()
|
||||
assert application_1.order == 3
|
||||
assert application_2.order == 2
|
||||
assert application_3.order == 1
|
||||
|
||||
send_mock.assert_called_once()
|
||||
assert send_mock.call_args[1]["group"].id == group.id
|
||||
assert send_mock.call_args[1]["user"].id == user.id
|
||||
assert send_mock.call_args[1]["order"] == [
|
||||
application_3.id,
|
||||
application_2.id,
|
||||
application_1.id,
|
||||
]
|
||||
|
||||
handler.order_applications(
|
||||
user=user,
|
||||
group=group,
|
||||
order=[application_1.id, application_3.id, application_2.id],
|
||||
)
|
||||
application_1.refresh_from_db()
|
||||
application_2.refresh_from_db()
|
||||
application_3.refresh_from_db()
|
||||
assert application_1.order == 1
|
||||
assert application_2.order == 3
|
||||
assert application_3.order == 2
|
||||
|
||||
handler.order_applications(user=user, group=group, order=[application_1.id])
|
||||
application_1.refresh_from_db()
|
||||
application_2.refresh_from_db()
|
||||
application_3.refresh_from_db()
|
||||
assert application_1.order == 1
|
||||
assert application_2.order == 0
|
||||
assert application_3.order == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@patch("baserow.core.signals.application_deleted.send")
|
||||
def test_delete_database_application(send_mock, data_fixture):
|
||||
|
|
|
@ -133,3 +133,19 @@ def test_application_deleted(mock_broadcast_to_group, data_fixture):
|
|||
assert args[0][0] == database.group_id
|
||||
assert args[0][1]["type"] == "application_deleted"
|
||||
assert args[0][1]["application_id"] == database_id
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.signals.broadcast_to_group")
|
||||
def test_applications_reordered(mock_broadcast_to_channel_group, data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
group = data_fixture.create_group(user=user)
|
||||
database = data_fixture.create_database_application(group=group)
|
||||
CoreHandler().order_applications(user=user, group=group, order=[database.id])
|
||||
|
||||
mock_broadcast_to_channel_group.delay.assert_called_once()
|
||||
args = mock_broadcast_to_channel_group.delay.call_args
|
||||
assert args[0][0] == database.group_id
|
||||
assert args[0][1]["type"] == "applications_reordered"
|
||||
assert args[0][1]["group_id"] == group.id
|
||||
assert args[0][1]["order"] == [database.id]
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
* Made it possible to import a JSON file when creating a table.
|
||||
* Made it possible to order the views by drag and drop.
|
||||
* Made it possible to order the groups by drag and drop.
|
||||
* Made it possible to order the applications by drag and drop.
|
||||
* Made it possible to order the tables by drag and drop.
|
||||
|
||||
## Released (2021-05-11)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
.tree {
|
||||
position: relative;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 12px;
|
||||
|
@ -57,7 +58,6 @@
|
|||
@extend %tree__size;
|
||||
@extend %ellipsis;
|
||||
|
||||
display: block;
|
||||
color: $color-primary-900;
|
||||
font-size: 14px;
|
||||
|
||||
|
|
|
@ -62,8 +62,10 @@
|
|||
>
|
||||
<div class="tree__action sidebar__action">
|
||||
<nuxt-link :to="{ name: 'dashboard' }" class="tree__link">
|
||||
<i class="tree__icon fas fa-tachometer-alt"></i>
|
||||
<span class="sidebar__item-name">Dashboard</span>
|
||||
<div>
|
||||
<i class="tree__icon fas fa-tachometer-alt"></i>
|
||||
<span class="sidebar__item-name">Dashboard</span>
|
||||
</div>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</li>
|
||||
|
@ -73,8 +75,10 @@
|
|||
:class="{ 'tree__action--disabled': isAdminPage }"
|
||||
>
|
||||
<a class="tree__link" @click.prevent="admin()">
|
||||
<i class="tree__icon fas fa-users-cog"></i>
|
||||
<span class="sidebar__item-name">Admin</span>
|
||||
<div>
|
||||
<i class="tree__icon fas fa-users-cog"></i>
|
||||
<span class="sidebar__item-name">Admin</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<ul v-show="isAdminPage" class="tree sidebar__tree">
|
||||
|
@ -93,11 +97,15 @@
|
|||
:to="{ name: adminType.routeName }"
|
||||
class="tree__link"
|
||||
>
|
||||
<i
|
||||
class="tree__icon fas"
|
||||
:class="'fa-' + adminType.iconClass"
|
||||
></i>
|
||||
<span class="sidebar__item-name">{{ adminType.name }}</span>
|
||||
<div>
|
||||
<i
|
||||
class="tree__icon fas"
|
||||
:class="'fa-' + adminType.iconClass"
|
||||
></i>
|
||||
<span class="sidebar__item-name">{{
|
||||
adminType.name
|
||||
}}</span>
|
||||
</div>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</li>
|
||||
|
@ -117,7 +125,7 @@
|
|||
0
|
||||
)
|
||||
"
|
||||
>{{ selectedGroup.name }}</a
|
||||
><div>{{ selectedGroup.name }}</div></a
|
||||
>
|
||||
<GroupsContext ref="groupSelect"></GroupsContext>
|
||||
</div>
|
||||
|
@ -125,8 +133,10 @@
|
|||
<li v-if="selectedGroup.permissions === 'ADMIN'" class="tree__item">
|
||||
<div class="tree__action">
|
||||
<a class="tree__link" @click="$refs.groupMembersModal.show()">
|
||||
<i class="tree__icon tree__icon--type fas fa-users"></i>
|
||||
Invite others
|
||||
<div>
|
||||
<i class="tree__icon tree__icon--type fas fa-users"></i>
|
||||
Invite others
|
||||
</div>
|
||||
</a>
|
||||
<GroupMembersModal
|
||||
ref="groupMembersModal"
|
||||
|
@ -139,6 +149,11 @@
|
|||
:is="getApplicationComponent(application)"
|
||||
v-for="application in applications"
|
||||
:key="application.id"
|
||||
v-sortable="{
|
||||
id: application.id,
|
||||
update: orderApplications,
|
||||
handle: '[data-sortable-handle]',
|
||||
}"
|
||||
:application="application"
|
||||
></component>
|
||||
</ul>
|
||||
|
@ -175,7 +190,7 @@
|
|||
<a
|
||||
class="tree__link tree__link--group"
|
||||
@click="$store.dispatch('group/select', group)"
|
||||
>{{ group.name }}</a
|
||||
><div>{{ group.name }}</div></a
|
||||
>
|
||||
<i class="tree__right-icon fas fa-arrow-right"></i>
|
||||
</div>
|
||||
|
@ -218,6 +233,7 @@
|
|||
<script>
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
|
||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||
import SettingsModal from '@baserow/modules/core/components/settings/SettingsModal'
|
||||
import SidebarApplication from '@baserow/modules/core/components/sidebar/SidebarApplication'
|
||||
import CreateApplicationContext from '@baserow/modules/core/components/application/CreateApplicationContext'
|
||||
|
@ -243,7 +259,7 @@ export default {
|
|||
applications() {
|
||||
return this.$store.getters['application/getAllOfGroup'](
|
||||
this.selectedGroup
|
||||
)
|
||||
).sort((a, b) => a.order - b.order)
|
||||
},
|
||||
adminTypes() {
|
||||
return this.$registry.getAll('admin')
|
||||
|
@ -302,6 +318,17 @@ export default {
|
|||
this.$nuxt.$router.push({ name: this.sortedAdminTypes[0].routeName })
|
||||
}
|
||||
},
|
||||
async orderApplications(order, oldOrder) {
|
||||
try {
|
||||
await this.$store.dispatch('application/order', {
|
||||
group: this.selectedGroup,
|
||||
order,
|
||||
oldOrder,
|
||||
})
|
||||
} catch (error) {
|
||||
notifyIf(error, 'application')
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -6,22 +6,25 @@
|
|||
'tree__item--loading': application._.loading,
|
||||
}"
|
||||
>
|
||||
<div class="tree__action tree__action--has-options">
|
||||
<div class="tree__action tree__action--has-options" data-sortable-handle>
|
||||
<a class="tree__link" @click="$emit('selected', application)">
|
||||
<i
|
||||
class="tree__icon tree__icon--type fas"
|
||||
:class="'fa-' + application._.type.iconClass"
|
||||
></i>
|
||||
<Editable
|
||||
ref="rename"
|
||||
:value="application.name"
|
||||
@change="renameApplication(application, $event)"
|
||||
></Editable>
|
||||
<div>
|
||||
<i
|
||||
class="tree__icon tree__icon--type fas"
|
||||
:class="'fa-' + application._.type.iconClass"
|
||||
></i>
|
||||
<Editable
|
||||
ref="rename"
|
||||
:value="application.name"
|
||||
@change="renameApplication(application, $event)"
|
||||
></Editable>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
ref="contextLink"
|
||||
class="tree__options"
|
||||
@click="$refs.context.toggle($refs.contextLink, 'bottom', 'right', 0)"
|
||||
@mousedown.stop
|
||||
>
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</a>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
</li>
|
||||
<component
|
||||
:is="getApplicationComponent(application)"
|
||||
v-for="application in applications"
|
||||
v-for="application in sortedApplications"
|
||||
:key="application.id"
|
||||
:application="application"
|
||||
:page="page"
|
||||
|
@ -60,6 +60,15 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
sortedApplications() {
|
||||
return this.applications
|
||||
.map((a) => a)
|
||||
.sort((a, b) => {
|
||||
return a.order - b.order
|
||||
})
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getApplicationComponent(application) {
|
||||
return this.$registry
|
||||
|
|
|
@ -8,11 +8,13 @@
|
|||
>
|
||||
<div class="tree__action">
|
||||
<a class="tree__link">
|
||||
<i
|
||||
class="tree__icon tree__icon--type fas"
|
||||
:class="'fa-' + application._.type.iconClass"
|
||||
></i>
|
||||
{{ application.name }}
|
||||
<div>
|
||||
<i
|
||||
class="tree__icon tree__icon--type fas"
|
||||
:class="'fa-' + application._.type.iconClass"
|
||||
></i>
|
||||
{{ application.name }}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<template
|
||||
|
|
|
@ -230,6 +230,13 @@ export class RealTimeHandler {
|
|||
store.dispatch('application/forceDelete', application)
|
||||
}
|
||||
})
|
||||
|
||||
this.registerEvent('applications_reordered', ({ store }, data) => {
|
||||
const group = store.getters['group/get'](data.group_id)
|
||||
if (group !== undefined) {
|
||||
store.commit('application/ORDER_ITEMS', { group, order: data.order })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,11 @@ export default (client) => {
|
|||
update(applicationId, values) {
|
||||
return client.patch(`/applications/${applicationId}/`, values)
|
||||
},
|
||||
order(groupId, order) {
|
||||
return client.post(`/applications/group/${groupId}/order/`, {
|
||||
application_ids: order,
|
||||
})
|
||||
},
|
||||
delete(applicationId) {
|
||||
return client.delete(`/applications/${applicationId}/`)
|
||||
},
|
||||
|
|
|
@ -40,6 +40,14 @@ export const mutations = {
|
|||
const index = state.items.findIndex((item) => item.id === id)
|
||||
Object.assign(state.items[index], state.items[index], values)
|
||||
},
|
||||
ORDER_ITEMS(state, { group, order }) {
|
||||
state.items
|
||||
.filter((item) => item.group.id === group.id)
|
||||
.forEach((item) => {
|
||||
const index = order.findIndex((value) => value === item.id)
|
||||
item.order = index === -1 ? 0 : index + 1
|
||||
})
|
||||
},
|
||||
DELETE_ITEM(state, id) {
|
||||
const index = state.items.findIndex((item) => item.id === id)
|
||||
state.items.splice(index, 1)
|
||||
|
@ -171,6 +179,20 @@ export const actions = {
|
|||
data = type.prepareForStoreUpdate(application, data)
|
||||
commit('UPDATE_ITEM', { id: application.id, values: data })
|
||||
},
|
||||
/**
|
||||
* Updates the order of all the applications in a group.
|
||||
*/
|
||||
async order({ commit, getters }, { group, order, oldOrder }) {
|
||||
commit('ORDER_ITEMS', { group, order })
|
||||
|
||||
try {
|
||||
await ApplicationService(this.$client).order(group.id, order)
|
||||
} catch (error) {
|
||||
commit('ORDER_ITEMS', { group, order: oldOrder })
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes an existing application.
|
||||
*/
|
||||
|
|
|
@ -8,17 +8,19 @@
|
|||
>
|
||||
<div class="tree__action">
|
||||
<a class="tree__link" @click="$emit('selected', application)">
|
||||
<i
|
||||
class="tree__icon tree__icon--type fas"
|
||||
:class="'fa-' + application._.type.iconClass"
|
||||
></i>
|
||||
{{ application.name }}
|
||||
<div>
|
||||
<i
|
||||
class="tree__icon tree__icon--type fas"
|
||||
:class="'fa-' + application._.type.iconClass"
|
||||
></i>
|
||||
{{ application.name }}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<template v-if="application._.selected">
|
||||
<ul class="tree__subs">
|
||||
<li
|
||||
v-for="table in application.tables"
|
||||
v-for="table in orderedTables"
|
||||
:key="table.id"
|
||||
class="tree__sub"
|
||||
:class="{ active: isTableActive(table) }"
|
||||
|
@ -49,6 +51,13 @@ export default {
|
|||
validator: (prop) => typeof prop === 'object' || prop === null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
orderedTables() {
|
||||
return this.application.tables
|
||||
.map((table) => table)
|
||||
.sort((a, b) => a.order - b.order)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
selectTable(application, table) {
|
||||
this.$emit('selected-page', {
|
||||
|
|
Loading…
Add table
Reference in a new issue