mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-17 10:22:36 +00:00
created groups listing endpoint, list groups in context menu and made context and modals stackable
This commit is contained in:
parent
3b1d94e23f
commit
38b2a697dc
36 changed files with 616 additions and 238 deletions
backend
src/baserow
api/v0
config
core
tests
baserow
api/v0
core
fixtures
web-frontend
20
backend/src/baserow/api/v0/groups/serializers.py
Normal file
20
backend/src/baserow/api/v0/groups/serializers.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from baserow.core.models import Group, GroupUser
|
||||||
|
|
||||||
|
|
||||||
|
class GroupSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Group
|
||||||
|
fields = ('id', 'name',)
|
||||||
|
|
||||||
|
|
||||||
|
class GroupUserSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = GroupUser
|
||||||
|
fields = ('order',)
|
||||||
|
|
||||||
|
def to_representation(self, instance):
|
||||||
|
data = super().to_representation(instance)
|
||||||
|
data.update(GroupSerializer(instance.group).data)
|
||||||
|
return data
|
10
backend/src/baserow/api/v0/groups/urls.py
Normal file
10
backend/src/baserow/api/v0/groups/urls.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
from django.conf.urls import url
|
||||||
|
|
||||||
|
from .views import GroupsView
|
||||||
|
|
||||||
|
|
||||||
|
app_name = 'baserow.api.v0.group'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^$', GroupsView.as_view(), name='list')
|
||||||
|
]
|
19
backend/src/baserow/api/v0/groups/views.py
Normal file
19
backend/src/baserow/api/v0/groups/views.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
|
||||||
|
from baserow.core.models import GroupUser
|
||||||
|
|
||||||
|
from .serializers import GroupUserSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class GroupsView(APIView):
|
||||||
|
permission_classes = (IsAuthenticated,)
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def get(self, request):
|
||||||
|
groups = GroupUser.objects.filter(user=request.user).select_related('group')
|
||||||
|
serializer = GroupUserSerializer(groups, many=True)
|
||||||
|
return Response(serializer.data)
|
|
@ -1,10 +1,12 @@
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
|
|
||||||
from .user import urls as user_urls
|
from .user import urls as user_urls
|
||||||
|
from .groups import urls as group_urls
|
||||||
|
|
||||||
|
|
||||||
app_name = 'baserow.api.v0'
|
app_name = 'baserow.api.v0'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('user/', include(user_urls, namespace='user'))
|
path('user/', include(user_urls, namespace='user')),
|
||||||
|
path('groups/', include(group_urls, namespace='groups'))
|
||||||
]
|
]
|
||||||
|
|
|
@ -25,6 +25,7 @@ INSTALLED_APPS = [
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
'corsheaders',
|
'corsheaders',
|
||||||
|
|
||||||
|
'baserow.core',
|
||||||
'baserow.api.v0'
|
'baserow.api.v0'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -3,5 +3,5 @@ from django.conf.urls import url
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^api/v0/', include('baserow.api.v0.urls', namespace='api')),
|
url(r'^api/v0/', include('baserow.api.v0.urls', namespace='api_v0')),
|
||||||
]
|
]
|
||||||
|
|
1
backend/src/baserow/core/__init__.py
Normal file
1
backend/src/baserow/core/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
app_name = 'baserow.group'
|
5
backend/src/baserow/core/apps.py
Normal file
5
backend/src/baserow/core/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ApiConfig(AppConfig):
|
||||||
|
name = 'baserow.core'
|
8
backend/src/baserow/core/managers.py
Normal file
8
backend/src/baserow/core/managers.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class GroupQuerySet(models.QuerySet):
|
||||||
|
def of_user(self, user):
|
||||||
|
return self.filter(
|
||||||
|
users__exact=user
|
||||||
|
).order_by('groupuser__order')
|
48
backend/src/baserow/core/migrations/0001_initial.py
Normal file
48
backend/src/baserow/core/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
# Generated by Django 2.2.2 on 2019-07-28 13:08
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Group',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True,
|
||||||
|
serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='GroupUser',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True,
|
||||||
|
serialize=False, verbose_name='ID')),
|
||||||
|
('order', models.PositiveIntegerField()),
|
||||||
|
('group', models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to='core.Group')),
|
||||||
|
('user', models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.AUTH_USER_MODEL
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ('order',),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='group',
|
||||||
|
name='users',
|
||||||
|
field=models.ManyToManyField(through='core.GroupUser',
|
||||||
|
to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
0
backend/src/baserow/core/migrations/__init__.py
Normal file
0
backend/src/baserow/core/migrations/__init__.py
Normal file
32
backend/src/baserow/core/models.py
Normal file
32
backend/src/baserow/core/models.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
from django.db import models
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
from .managers import GroupQuerySet
|
||||||
|
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class Group(models.Model):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
users = models.ManyToManyField(User, through='GroupUser')
|
||||||
|
|
||||||
|
objects = GroupQuerySet.as_manager()
|
||||||
|
|
||||||
|
|
||||||
|
class GroupUser(models.Model):
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
group = models.ForeignKey(Group, on_delete=models.CASCADE)
|
||||||
|
order = models.PositiveIntegerField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('order',)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_last_order(cls, user):
|
||||||
|
"""Returns a new position that will be last for a new group."""
|
||||||
|
return cls.objects.filter(
|
||||||
|
user=user
|
||||||
|
).aggregate(
|
||||||
|
models.Max('order')
|
||||||
|
).get('order__max', 0) + 1
|
31
backend/tests/baserow/api/v0/group/test_group_views.py
Normal file
31
backend/tests/baserow/api/v0/group/test_group_views.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from django.shortcuts import reverse
|
||||||
|
|
||||||
|
from rest_framework_jwt.settings import api_settings
|
||||||
|
|
||||||
|
|
||||||
|
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
|
||||||
|
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_list_groups(client, data_fixture, django_assert_num_queries):
|
||||||
|
user = data_fixture.create_user(email='test@test.nl', password='password',
|
||||||
|
first_name='Test1')
|
||||||
|
group_2 = data_fixture.create_user_group(user=user, order=2)
|
||||||
|
group_1 = data_fixture.create_user_group(user=user, order=1)
|
||||||
|
|
||||||
|
payload = jwt_payload_handler(user)
|
||||||
|
token = jwt_encode_handler(payload)
|
||||||
|
|
||||||
|
with django_assert_num_queries(4):
|
||||||
|
response = client.get(reverse('api_v0:groups:list'), **{
|
||||||
|
'HTTP_AUTHORIZATION': f'JWT {token}'
|
||||||
|
})
|
||||||
|
assert response.status_code == 200
|
||||||
|
response_json = response.json()
|
||||||
|
assert response_json[0]['id'] == group_1.id
|
||||||
|
assert response_json[0]['order'] == 1
|
||||||
|
assert response_json[1]['id'] == group_2.id
|
||||||
|
assert response_json[1]['order'] == 2
|
|
@ -20,7 +20,7 @@ def test_token_auth(client, data_fixture):
|
||||||
data_fixture.create_user(email='test@test.nl', password='password',
|
data_fixture.create_user(email='test@test.nl', password='password',
|
||||||
first_name='Test1')
|
first_name='Test1')
|
||||||
|
|
||||||
response = client.post(reverse('api:user:token_auth'), {
|
response = client.post(reverse('api_v0:user:token_auth'), {
|
||||||
'username': 'no_existing@test.nl',
|
'username': 'no_existing@test.nl',
|
||||||
'password': 'password'
|
'password': 'password'
|
||||||
})
|
})
|
||||||
|
@ -29,7 +29,7 @@ def test_token_auth(client, data_fixture):
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
assert len(json['non_field_errors']) > 0
|
assert len(json['non_field_errors']) > 0
|
||||||
|
|
||||||
response = client.post(reverse('api:user:token_auth'), {
|
response = client.post(reverse('api_v0:user:token_auth'), {
|
||||||
'username': 'test@test.nl',
|
'username': 'test@test.nl',
|
||||||
'password': 'wrong_password'
|
'password': 'wrong_password'
|
||||||
})
|
})
|
||||||
|
@ -38,7 +38,7 @@ def test_token_auth(client, data_fixture):
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
assert len(json['non_field_errors']) > 0
|
assert len(json['non_field_errors']) > 0
|
||||||
|
|
||||||
response = client.post(reverse('api:user:token_auth'), {
|
response = client.post(reverse('api_v0:user:token_auth'), {
|
||||||
'username': 'test@test.nl',
|
'username': 'test@test.nl',
|
||||||
'password': 'password'
|
'password': 'password'
|
||||||
})
|
})
|
||||||
|
@ -56,13 +56,13 @@ def test_token_refresh(client, data_fixture):
|
||||||
user = data_fixture.create_user(email='test@test.nl', password='password',
|
user = data_fixture.create_user(email='test@test.nl', password='password',
|
||||||
first_name='Test1')
|
first_name='Test1')
|
||||||
|
|
||||||
response = client.post(reverse('api:user:token_refresh'), {'token': 'WRONG_TOKEN'})
|
response = client.post(reverse('api_v0:user:token_refresh'), {'token': 'WRONG_TOKEN'})
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
|
|
||||||
payload = jwt_payload_handler(user)
|
payload = jwt_payload_handler(user)
|
||||||
token = jwt_encode_handler(payload)
|
token = jwt_encode_handler(payload)
|
||||||
|
|
||||||
response = client.post(reverse('api:user:token_refresh'), {'token': token})
|
response = client.post(reverse('api_v0:user:token_refresh'), {'token': token})
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert 'token' in response.json()
|
assert 'token' in response.json()
|
||||||
|
|
||||||
|
@ -71,5 +71,5 @@ def test_token_refresh(client, data_fixture):
|
||||||
payload = jwt_payload_handler(user)
|
payload = jwt_payload_handler(user)
|
||||||
token = jwt_encode_handler(payload)
|
token = jwt_encode_handler(payload)
|
||||||
|
|
||||||
response = client.post(reverse('api:user:token_refresh'), {'token': token})
|
response = client.post(reverse('api_v0:user:token_refresh'), {'token': token})
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
|
|
|
@ -9,7 +9,7 @@ User = get_user_model()
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_create_user(client):
|
def test_create_user(client):
|
||||||
response = client.post(reverse('api:user:index'), {
|
response = client.post(reverse('api_v0:user:index'), {
|
||||||
'name': 'Test1',
|
'name': 'Test1',
|
||||||
'email': 'test@test.nl',
|
'email': 'test@test.nl',
|
||||||
'password': 'test12'
|
'password': 'test12'
|
||||||
|
@ -21,7 +21,7 @@ def test_create_user(client):
|
||||||
assert user.email == 'test@test.nl'
|
assert user.email == 'test@test.nl'
|
||||||
assert user.password != ''
|
assert user.password != ''
|
||||||
|
|
||||||
response_failed = client.post(reverse('api:user:index'), {
|
response_failed = client.post(reverse('api_v0:user:index'), {
|
||||||
'name': 'Test1',
|
'name': 'Test1',
|
||||||
'email': 'test@test.nl',
|
'email': 'test@test.nl',
|
||||||
'password': 'test12'
|
'password': 'test12'
|
||||||
|
@ -30,7 +30,7 @@ def test_create_user(client):
|
||||||
assert response_failed.status_code == 400
|
assert response_failed.status_code == 400
|
||||||
assert response_failed.json()['error'] == 'ERROR_ALREADY_EXISTS'
|
assert response_failed.json()['error'] == 'ERROR_ALREADY_EXISTS'
|
||||||
|
|
||||||
response_failed_2 = client.post(reverse('api:user:index'), {
|
response_failed_2 = client.post(reverse('api_v0:user:index'), {
|
||||||
'email': 'test'
|
'email': 'test'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
24
backend/tests/baserow/core/test_core_managers.py
Normal file
24
backend/tests/baserow/core/test_core_managers.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from baserow.core.models import Group
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_groups_of_user(data_fixture):
|
||||||
|
user_1 = data_fixture.create_user()
|
||||||
|
group_user_1 = data_fixture.create_user_group(user=user_1, order=1)
|
||||||
|
group_user_2 = data_fixture.create_user_group(user=user_1, order=2)
|
||||||
|
group_user_3 = data_fixture.create_user_group(user=user_1, order=0)
|
||||||
|
|
||||||
|
user_2 = data_fixture.create_user()
|
||||||
|
group_user_4 = data_fixture.create_user_group(user=user_2, order=0)
|
||||||
|
|
||||||
|
groups_user_1 = Group.objects.of_user(user=user_1)
|
||||||
|
assert len(groups_user_1) == 3
|
||||||
|
assert groups_user_1[0].id == group_user_3.id
|
||||||
|
assert groups_user_1[1].id == group_user_1.id
|
||||||
|
assert groups_user_1[2].id == group_user_2.id
|
||||||
|
|
||||||
|
groups_user_2 = Group.objects.of_user(user=user_2)
|
||||||
|
assert len(groups_user_2) == 1
|
||||||
|
assert groups_user_2[0].id == group_user_4.id
|
13
backend/tests/baserow/core/test_core_models.py
Normal file
13
backend/tests/baserow/core/test_core_models.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from baserow.core.models import GroupUser
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_group_user_get_next_order(data_fixture):
|
||||||
|
group_user_1 = data_fixture.create_user_group(order=0)
|
||||||
|
group_user_2_1 = data_fixture.create_user_group(order=10)
|
||||||
|
group_user_2_2 = data_fixture.create_user_group(user=group_user_2_1.user, order=11)
|
||||||
|
|
||||||
|
assert GroupUser.get_last_order(group_user_1.user) == 1
|
||||||
|
assert GroupUser.get_last_order(group_user_2_1.user) == 12
|
7
backend/tests/fixtures/__init__.py
vendored
7
backend/tests/fixtures/__init__.py
vendored
|
@ -1,5 +1,8 @@
|
||||||
|
from faker import Faker
|
||||||
|
|
||||||
from .user import UserFixtures
|
from .user import UserFixtures
|
||||||
|
from .group import GroupFixtures
|
||||||
|
|
||||||
|
|
||||||
class Fixtures(UserFixtures):
|
class Fixtures(UserFixtures, GroupFixtures):
|
||||||
pass
|
fake = Faker()
|
||||||
|
|
24
backend/tests/fixtures/group.py
vendored
Normal file
24
backend/tests/fixtures/group.py
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
from baserow.core.models import Group, GroupUser
|
||||||
|
|
||||||
|
|
||||||
|
class GroupFixtures:
|
||||||
|
def create_group(self, **kwargs):
|
||||||
|
user = kwargs.pop('user', None)
|
||||||
|
users = kwargs.pop('users', [])
|
||||||
|
|
||||||
|
if user:
|
||||||
|
users.insert(0, user)
|
||||||
|
|
||||||
|
kwargs.setdefault('name', self.fake.name())
|
||||||
|
group = Group.objects.create(**kwargs)
|
||||||
|
|
||||||
|
for user in users:
|
||||||
|
self.create_user_group(group=group, user=user, order=0)
|
||||||
|
|
||||||
|
return group
|
||||||
|
|
||||||
|
def create_user_group(self, **kwargs):
|
||||||
|
kwargs.setdefault('group', self.create_group())
|
||||||
|
kwargs.setdefault('user', self.create_user())
|
||||||
|
kwargs.setdefault('order', 0)
|
||||||
|
return GroupUser.objects.create(**kwargs)
|
6
backend/tests/fixtures/user.py
vendored
6
backend/tests/fixtures/user.py
vendored
|
@ -1,16 +1,14 @@
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from faker import Faker
|
|
||||||
|
|
||||||
|
|
||||||
fake = Faker()
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class UserFixtures:
|
class UserFixtures:
|
||||||
def create_user(self, **kwargs):
|
def create_user(self, **kwargs):
|
||||||
kwargs.setdefault('email', fake.email())
|
kwargs.setdefault('email', self.fake.email())
|
||||||
kwargs.setdefault('username', kwargs['email'])
|
kwargs.setdefault('username', kwargs['email'])
|
||||||
kwargs.setdefault('first_name', fake.name())
|
kwargs.setdefault('first_name', self.fake.name())
|
||||||
kwargs.setdefault('password', 'password')
|
kwargs.setdefault('password', 'password')
|
||||||
|
|
||||||
user = User(**kwargs)
|
user = User(**kwargs)
|
||||||
|
|
|
@ -63,11 +63,12 @@ $z-index-layout-col-2: 2;
|
||||||
$z-index-layout-col-3: 1;
|
$z-index-layout-col-3: 1;
|
||||||
$z-index-layout-col-3-1: 5;
|
$z-index-layout-col-3-1: 5;
|
||||||
$z-index-layout-col-3-2: 4;
|
$z-index-layout-col-3-2: 4;
|
||||||
$z-index-modal: 6;
|
|
||||||
|
|
||||||
// The z-index of the context must always be the highest because they can open in a
|
// The z-index of modal and context must always be the same and the highest because they
|
||||||
// modal.
|
// can be nested inside each other. The order in html decided which must be shown over
|
||||||
$z-index-context: 7;
|
// the other.
|
||||||
|
$z-index-modal: 6;
|
||||||
|
$z-index-context: 6;
|
||||||
|
|
||||||
// normalize overrides
|
// normalize overrides
|
||||||
$base-font-family: $text-font-stack;
|
$base-font-family: $text-font-stack;
|
||||||
|
|
|
@ -8,6 +8,17 @@
|
||||||
box-shadow: 0 2px 6px 0 rgba($black, 0.16);
|
box-shadow: 0 2px 6px 0 rgba($black, 0.16);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.context-loading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 32px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-description {
|
||||||
|
padding: 32px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.context-menu-title {
|
.context-menu-title {
|
||||||
color: $color-neutral-600;
|
color: $color-neutral-600;
|
||||||
padding: 12px 8px 2px 8px;
|
padding: 12px 8px 2px 8px;
|
||||||
|
|
11
web-frontend/assets/scss/components/_loading.scss
Normal file
11
web-frontend/assets/scss/components/_loading.scss
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
.loading {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
width: 1.4em;
|
||||||
|
height: 1.4em;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 0.25em solid;
|
||||||
|
border-color: $color-primary-500 transparent $color-primary-500 transparent;
|
||||||
|
animation: spin infinite 1800ms;
|
||||||
|
animation-timing-function: cubic-bezier(0.785, 0.135, 0.15, 0.86);
|
||||||
|
}
|
|
@ -26,3 +26,4 @@
|
||||||
@import 'components/views/grid/boolean';
|
@import 'components/views/grid/boolean';
|
||||||
@import 'components/views/grid/number';
|
@import 'components/views/grid/number';
|
||||||
@import 'components/box_page';
|
@import 'components/box_page';
|
||||||
|
@import 'components/loading';
|
||||||
|
|
|
@ -6,9 +6,11 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { isElement } from '@/utils/dom'
|
import { isElement } from '@/utils/dom'
|
||||||
|
import MoveToBody from '@/mixins/moveToBody'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Context',
|
name: 'Context',
|
||||||
|
mixins: [MoveToBody],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
open: false,
|
open: false,
|
||||||
|
@ -16,36 +18,6 @@ export default {
|
||||||
children: []
|
children: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/**
|
|
||||||
* Because we don't want the parent context to close when a user clicks 'outside' that
|
|
||||||
* element and in the child element we need to register the child with their parent to
|
|
||||||
* prevent this.
|
|
||||||
*/
|
|
||||||
mounted() {
|
|
||||||
let $parent = this.$parent
|
|
||||||
while ($parent !== undefined) {
|
|
||||||
if ($parent.registerContextChild) {
|
|
||||||
$parent.registerContextChild(this.$el)
|
|
||||||
}
|
|
||||||
$parent = $parent.$parent
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move the rendered element to the top of the body so it can be positioned over any
|
|
||||||
// other element.
|
|
||||||
const body = document.body
|
|
||||||
body.insertBefore(this.$el, body.firstChild)
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Make sure the context menu is not open and all the events on the body are removed
|
|
||||||
* and that the element is removed from the body.
|
|
||||||
*/
|
|
||||||
destroyed() {
|
|
||||||
this.hide()
|
|
||||||
|
|
||||||
if (this.$el.parentNode) {
|
|
||||||
this.$el.parentNode.removeChild(this.$el)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
/**
|
/**
|
||||||
* Toggles the open state of the context menu.
|
* Toggles the open state of the context menu.
|
||||||
|
@ -54,6 +26,8 @@ export default {
|
||||||
* context, this will be used to calculate the correct position.
|
* context, this will be used to calculate the correct position.
|
||||||
* @param vertical Bottom positions the context under the target.
|
* @param vertical Bottom positions the context under the target.
|
||||||
* Top positions the context above the target.
|
* Top positions the context above the target.
|
||||||
|
* Over-bottom positions the context over and under the target.
|
||||||
|
* Over-top positions the context over and above the target
|
||||||
* @param horizontal Left aligns the context with the left side of the target.
|
* @param horizontal Left aligns the context with the left side of the target.
|
||||||
* Right aligns the context with the right side of the target.
|
* Right aligns the context with the right side of the target.
|
||||||
* @param offset The distance between the target element and the context.
|
* @param offset The distance between the target element and the context.
|
||||||
|
@ -136,6 +110,9 @@ export default {
|
||||||
const canTop = targetRect.top - contextRect.height - offset > 0
|
const canTop = targetRect.top - contextRect.height - offset > 0
|
||||||
const canBottom =
|
const canBottom =
|
||||||
window.innerHeight - targetRect.bottom - contextRect.height - offset > 0
|
window.innerHeight - targetRect.bottom - contextRect.height - offset > 0
|
||||||
|
const canOverTop = targetRect.bottom - contextRect.height - offset > 0
|
||||||
|
const canOverBottom =
|
||||||
|
window.innerHeight - targetRect.bottom - contextRect.height - offset > 0
|
||||||
const canRight = targetRect.right - contextRect.width > 0
|
const canRight = targetRect.right - contextRect.width > 0
|
||||||
const canLeft =
|
const canLeft =
|
||||||
window.innerWidth - targetRect.left - contextRect.width > 0
|
window.innerWidth - targetRect.left - contextRect.width > 0
|
||||||
|
@ -150,6 +127,14 @@ export default {
|
||||||
vertical = 'bottom'
|
vertical = 'bottom'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (vertical === 'over-bottom' && !canOverBottom && canOverTop) {
|
||||||
|
vertical = 'over-top'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vertical === 'over-top' && !canOverTop) {
|
||||||
|
vertical = 'over-bottom'
|
||||||
|
}
|
||||||
|
|
||||||
if (horizontal === 'left' && !canLeft && canRight) {
|
if (horizontal === 'left' && !canLeft && canRight) {
|
||||||
horizontal = 'right'
|
horizontal = 'right'
|
||||||
}
|
}
|
||||||
|
@ -175,6 +160,14 @@ export default {
|
||||||
positions.bottom = window.innerHeight - targetRect.top + offset
|
positions.bottom = window.innerHeight - targetRect.top + offset
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (vertical === 'over-bottom') {
|
||||||
|
positions.top = targetRect.top + offset
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vertical === 'over-top') {
|
||||||
|
positions.bottom = window.innerHeight - targetRect.bottom + offset
|
||||||
|
}
|
||||||
|
|
||||||
return positions
|
return positions
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
|
|
66
web-frontend/components/Modal.vue
Normal file
66
web-frontend/components/Modal.vue
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="modalWrapper"
|
||||||
|
:class="{ hidden: !open }"
|
||||||
|
class="modal-wrapper"
|
||||||
|
@click="outside($event)"
|
||||||
|
>
|
||||||
|
<div class="modal-box">
|
||||||
|
<a class="modal-close" @click="hide()">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</a>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MoveToBody from '@/mixins/moveToBody'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CreateGroupModal',
|
||||||
|
mixins: [MoveToBody],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
open: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* Toggle the open state of the modal.
|
||||||
|
*/
|
||||||
|
toggle(value) {
|
||||||
|
if (value === undefined) {
|
||||||
|
value = !this.open
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
this.show()
|
||||||
|
} else {
|
||||||
|
this.hide()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Show the modal.
|
||||||
|
*/
|
||||||
|
show() {
|
||||||
|
this.open = true
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Hide the modal.
|
||||||
|
*/
|
||||||
|
hide() {
|
||||||
|
this.open = false
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* If someone actually clicked on the modal wrapper and not one of his children the
|
||||||
|
* modal should be closed.
|
||||||
|
*/
|
||||||
|
outside(event) {
|
||||||
|
if (event.target === this.$refs.modalWrapper) {
|
||||||
|
this.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
27
web-frontend/components/group/CreateGroupModal.vue
Normal file
27
web-frontend/components/group/CreateGroupModal.vue
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<template>
|
||||||
|
<Modal ref="createGroupModal">
|
||||||
|
<h2 class="box-title">Create new group</h2>
|
||||||
|
<form>
|
||||||
|
<div class="control">
|
||||||
|
<label class="control-label">
|
||||||
|
<i class="fas fa-font"></i>
|
||||||
|
Name
|
||||||
|
</label>
|
||||||
|
<div class="control-elements">
|
||||||
|
<input type="text" class="input input-large" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'CreateGroupModal',
|
||||||
|
methods: {
|
||||||
|
show() {
|
||||||
|
this.$refs.createGroupModal.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
89
web-frontend/components/group/GroupsContext.vue
Normal file
89
web-frontend/components/group/GroupsContext.vue
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
<template>
|
||||||
|
<Context ref="groupContext" class="select">
|
||||||
|
<div class="select-search">
|
||||||
|
<i class="select-search-icon fas fa-search"></i>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="select-search-input"
|
||||||
|
placeholder="Search views"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="isLoading" class="context-loading">
|
||||||
|
<div class="loading"></div>
|
||||||
|
</div>
|
||||||
|
<ul v-if="!isLoading && groups.length > 0" class="select-items">
|
||||||
|
<li v-for="group in groups" :key="group.id" class="select-item">
|
||||||
|
<a href="#" class="select-item-link">{{ group.name }}</a>
|
||||||
|
<a
|
||||||
|
:ref="'groupOptions' + group.id"
|
||||||
|
class="select-item-options"
|
||||||
|
@click="toggleContext(group.id)"
|
||||||
|
>
|
||||||
|
<i class="fas fa-ellipsis-v"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div v-if="!isLoading && groups.length == 0" class="context-description">
|
||||||
|
No results found
|
||||||
|
</div>
|
||||||
|
<Context ref="groupsItemContext">
|
||||||
|
<ul class="context-menu">
|
||||||
|
<li>
|
||||||
|
<a href="#">
|
||||||
|
<i class="context-menu-icon fas fa-fw fa-pen"></i>
|
||||||
|
Rename group
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#">
|
||||||
|
<i class="context-menu-icon fas fa-fw fa-trash"></i>
|
||||||
|
Delete group
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Context>
|
||||||
|
<div class="select-footer">
|
||||||
|
<a class="select-footer-button" @click="$refs.createGroupModal.show()">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
Create group
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<CreateGroupModal ref="createGroupModal"></CreateGroupModal>
|
||||||
|
</Context>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters, mapState } from 'vuex'
|
||||||
|
|
||||||
|
import CreateGroupModal from '@/components/group/CreateGroupModal'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'GroupsItemContext',
|
||||||
|
components: {
|
||||||
|
CreateGroupModal
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
open: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
groups: state => state.group.items
|
||||||
|
}),
|
||||||
|
...mapGetters({
|
||||||
|
isLoading: 'group/isLoading'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggle(...args) {
|
||||||
|
this.$store.dispatch('group/loadAll')
|
||||||
|
this.$refs.groupContext.toggle(...args)
|
||||||
|
},
|
||||||
|
toggleContext(groupId) {
|
||||||
|
const target = this.$refs['groupOptions' + groupId][0]
|
||||||
|
this.$refs.groupsItemContext.toggle(target, 'bottom', 'right', 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -25,7 +25,11 @@ export default {
|
||||||
/*
|
/*
|
||||||
** Plugins to load before mounting the App
|
** Plugins to load before mounting the App
|
||||||
*/
|
*/
|
||||||
plugins: [{ src: '@/plugins/auth.js' }, { src: '@/plugins/vuelidate.js' }],
|
plugins: [
|
||||||
|
{ src: '@/plugins/global.js' },
|
||||||
|
{ src: '@/plugins/auth.js' },
|
||||||
|
{ src: '@/plugins/vuelidate.js' }
|
||||||
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
** Nuxt.js modules
|
** Nuxt.js modules
|
||||||
|
|
16
web-frontend/intellij-idea.webpack.config.js
Normal file
16
web-frontend/intellij-idea.webpack.config.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/** This file can be used in combination with intellij idea so the @ path resolves **/
|
||||||
|
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.js', '.json', '.vue', '.ts'],
|
||||||
|
root: path.resolve(__dirname),
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname),
|
||||||
|
'@@': path.resolve(__dirname),
|
||||||
|
'~': path.resolve(__dirname),
|
||||||
|
'~~': path.resolve(__dirname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,68 +9,15 @@
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</li>
|
</li>
|
||||||
<li class="menu-item">
|
<li class="menu-item">
|
||||||
<a href="#" class="menu-link" data-context=".select">
|
<a
|
||||||
|
ref="groupSelectToggle"
|
||||||
|
class="menu-link"
|
||||||
|
@click="$refs.groupSelect.toggle($refs.groupSelectToggle)"
|
||||||
|
>
|
||||||
<i class="fas fa-layer-group"></i>
|
<i class="fas fa-layer-group"></i>
|
||||||
<span class="menu-link-text">Groups</span>
|
<span class="menu-link-text">Groups</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="select hidden">
|
<GroupsContext ref="groupSelect"></GroupsContext>
|
||||||
<div class="select-search">
|
|
||||||
<i class="select-search-icon fas fa-search"></i>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="select-search-input"
|
|
||||||
placeholder="Search views"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<ul class="select-items">
|
|
||||||
<li class="select-item active">
|
|
||||||
<a href="#" class="select-item-link">Group name 1</a>
|
|
||||||
<a href="#" class="select-item-options" data-context=".context">
|
|
||||||
<i class="fas fa-ellipsis-v"></i>
|
|
||||||
</a>
|
|
||||||
<div class="context hidden">
|
|
||||||
<ul class="context-menu">
|
|
||||||
<li>
|
|
||||||
<a href="#">
|
|
||||||
<i class="context-menu-icon fas fa-fw fa-pen"></i>
|
|
||||||
Rename group
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#">
|
|
||||||
<i class="context-menu-icon fas fa-fw fa-trash"></i>
|
|
||||||
Delete group
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li class="select-item">
|
|
||||||
<a href="#" class="select-item-link">Group name 2</a>
|
|
||||||
<a href="#" class="select-item-options">
|
|
||||||
<i class="fas fa-ellipsis-v"></i>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="select-item">
|
|
||||||
<a href="#" class="select-item-link">Group name 3</a>
|
|
||||||
<a href="#" class="select-item-options">
|
|
||||||
<i class="fas fa-ellipsis-v"></i>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="select-item">
|
|
||||||
<a href="#" class="select-item-link">Group name 4</a>
|
|
||||||
<a href="#" class="select-item-options">
|
|
||||||
<i class="fas fa-ellipsis-v"></i>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div class="select-footer">
|
|
||||||
<a href="#" class="select-footer-button">
|
|
||||||
<i class="fas fa-plus"></i>
|
|
||||||
Do something
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="menu-items">
|
<ul class="menu-items">
|
||||||
|
@ -108,130 +55,6 @@
|
||||||
<div class="sidebar-title">
|
<div class="sidebar-title">
|
||||||
<img src="@/static/img/logo.svg" alt="" />
|
<img src="@/static/img/logo.svg" alt="" />
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-group-title">Group name 1</div>
|
|
||||||
<ul class="tree">
|
|
||||||
<li class="tree-item">
|
|
||||||
<div class="tree-action">
|
|
||||||
<a href="#" class="tree-link">
|
|
||||||
<i class="tree-type fas fa-database"></i>
|
|
||||||
Vehicles
|
|
||||||
</a>
|
|
||||||
<a href="#" class="tree-options" data-context=".context">
|
|
||||||
<i class="fas fa-ellipsis-v"></i>
|
|
||||||
</a>
|
|
||||||
<div class="context hidden">
|
|
||||||
<div class="context-menu-title">Vehicles</div>
|
|
||||||
<ul class="context-menu">
|
|
||||||
<li>
|
|
||||||
<a href="#">
|
|
||||||
<i class="context-menu-icon fas fa-fw fa-pen"></i>
|
|
||||||
Rename database
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#">
|
|
||||||
<i class="context-menu-icon fas fa-fw fa-trash"></i>
|
|
||||||
Delete table
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li class="tree-item">
|
|
||||||
<div class="tree-action">
|
|
||||||
<a href="#" class="tree-link">
|
|
||||||
<i class="tree-type fas fa-angle-right"></i>
|
|
||||||
Map nummer 1
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li class="tree-item active">
|
|
||||||
<div class="tree-action">
|
|
||||||
<a href="#" class="tree-link">
|
|
||||||
<i class="tree-type fas fa-database"></i>
|
|
||||||
Webshop
|
|
||||||
</a>
|
|
||||||
<a href="#" class="tree-options">
|
|
||||||
<i class="fas fa-ellipsis-v"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<ul class="tree-subs">
|
|
||||||
<li class="tree-sub active">
|
|
||||||
<a href="#" class="tree-sub-link">Customers</a>
|
|
||||||
<a href="#" class="tree-options">
|
|
||||||
<i class="fas fa-ellipsis-v"></i>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="tree-sub">
|
|
||||||
<a href="#" class="tree-sub-link">Products very long name</a>
|
|
||||||
<a href="#" class="tree-options">
|
|
||||||
<i class="fas fa-ellipsis-v"></i>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="tree-sub">
|
|
||||||
<a href="#" class="tree-sub-link">Categories</a>
|
|
||||||
<a href="#" class="tree-options">
|
|
||||||
<i class="fas fa-ellipsis-v"></i>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li class="tree-item">
|
|
||||||
<div class="tree-action">
|
|
||||||
<a href="#" class="tree-link">
|
|
||||||
<i class="tree-type fas fa-angle-down"></i>
|
|
||||||
Map nummer 1
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<ul class="tree">
|
|
||||||
<li class="tree-item">
|
|
||||||
<div class="tree-action">
|
|
||||||
<a href="#" class="tree-link">
|
|
||||||
<i class="tree-type fas fa-database"></i>
|
|
||||||
Vehicles
|
|
||||||
</a>
|
|
||||||
<a href="#" class="tree-options">
|
|
||||||
<i class="fas fa-ellipsis-v"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li class="tree-item">
|
|
||||||
<div class="tree-action">
|
|
||||||
<a href="#" class="tree-link">
|
|
||||||
<i class="tree-type fas fa-database"></i>
|
|
||||||
Something
|
|
||||||
</a>
|
|
||||||
<a href="#" class="tree-options">
|
|
||||||
<i class="fas fa-ellipsis-v"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li class="tree-item">
|
|
||||||
<div class="tree-action">
|
|
||||||
<a href="#" class="tree-link">
|
|
||||||
<i class="tree-type fas fa-database"></i>
|
|
||||||
Vehicles with very long name and that is not good.
|
|
||||||
</a>
|
|
||||||
<a href="#" class="tree-options">
|
|
||||||
<i class="fas fa-ellipsis-v"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li class="tree-item">
|
|
||||||
<div class="tree-action">
|
|
||||||
<a href="#" class="tree-link">
|
|
||||||
<i class="tree-type fas fa-database"></i>
|
|
||||||
Something else
|
|
||||||
</a>
|
|
||||||
<a href="#" class="tree-options">
|
|
||||||
<i class="fas fa-ellipsis-v"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-footer">
|
<div class="sidebar-footer">
|
||||||
|
@ -250,13 +73,13 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapActions, mapGetters } from 'vuex'
|
import { mapActions, mapGetters } from 'vuex'
|
||||||
|
|
||||||
import Context from '@/components/Context'
|
import GroupsContext from '@/components/group/GroupsContext'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
layout: 'default',
|
layout: 'default',
|
||||||
middleware: 'authenticated',
|
middleware: 'authenticated',
|
||||||
components: {
|
components: {
|
||||||
Context
|
GroupsContext
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
|
|
32
web-frontend/mixins/moveToBody.js
Normal file
32
web-frontend/mixins/moveToBody.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* Because we don't want the parent context to close when a user clicks 'outside' that
|
||||||
|
* element and in the child element we need to register the child with their parent to
|
||||||
|
* prevent this.
|
||||||
|
*/
|
||||||
|
mounted() {
|
||||||
|
let $parent = this.$parent
|
||||||
|
while ($parent !== undefined) {
|
||||||
|
if ($parent.registerContextChild) {
|
||||||
|
$parent.registerContextChild(this.$el)
|
||||||
|
}
|
||||||
|
$parent = $parent.$parent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move the rendered element to the top of the body so it can be positioned over any
|
||||||
|
// other element.
|
||||||
|
const body = document.body
|
||||||
|
body.insertBefore(this.$el, body.firstChild)
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Make sure the context menu is not open and all the events on the body are removed
|
||||||
|
* and that the element is removed from the body.
|
||||||
|
*/
|
||||||
|
destroyed() {
|
||||||
|
this.hide()
|
||||||
|
|
||||||
|
if (this.$el.parentNode) {
|
||||||
|
this.$el.parentNode.removeChild(this.$el)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ export default function({ store }) {
|
||||||
client.interceptors.request.use(config => {
|
client.interceptors.request.use(config => {
|
||||||
if (store.getters['auth/isAuthenticated']) {
|
if (store.getters['auth/isAuthenticated']) {
|
||||||
const token = store.getters['auth/token']
|
const token = store.getters['auth/token']
|
||||||
config.headers.Authorization = `JWT: ${token}`
|
config.headers.Authorization = `JWT ${token}`
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
})
|
})
|
||||||
|
|
7
web-frontend/plugins/global.js
Normal file
7
web-frontend/plugins/global.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
import Context from '@/components/Context'
|
||||||
|
import Modal from '@/components/Modal'
|
||||||
|
|
||||||
|
Vue.component('Context', Context)
|
||||||
|
Vue.component('Modal', Modal)
|
7
web-frontend/services/group.js
Normal file
7
web-frontend/services/group.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { client } from './client'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
fetchAll() {
|
||||||
|
return client.get('/groups/')
|
||||||
|
}
|
||||||
|
}
|
51
web-frontend/store/group.js
Normal file
51
web-frontend/store/group.js
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import GroupService from '@/services/group'
|
||||||
|
|
||||||
|
export const state = () => ({
|
||||||
|
loaded: false,
|
||||||
|
loading: false,
|
||||||
|
items: []
|
||||||
|
})
|
||||||
|
|
||||||
|
export const mutations = {
|
||||||
|
SET_LOADED(state, loaded) {
|
||||||
|
state.loaded = loaded
|
||||||
|
},
|
||||||
|
SET_LOADING(state, loading) {
|
||||||
|
state.loading = loading
|
||||||
|
},
|
||||||
|
SET_ITEMS(state, items) {
|
||||||
|
state.items = items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
loadAll({ state, dispatch }) {
|
||||||
|
if (!state.loaded && !state.loading) {
|
||||||
|
dispatch('fetchAll')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fetchAll({ commit }) {
|
||||||
|
commit('SET_LOADING', true)
|
||||||
|
|
||||||
|
return GroupService.fetchAll()
|
||||||
|
.then(({ data }) => {
|
||||||
|
commit('SET_LOADED', false)
|
||||||
|
commit('SET_ITEMS', data)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
commit('SET_ITEMS', [])
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
commit('SET_LOADING', false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getters = {
|
||||||
|
isLoaded(state) {
|
||||||
|
return state.loaded
|
||||||
|
},
|
||||||
|
isLoading(state) {
|
||||||
|
return state.loading
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue