baiscs of space edit page

This commit is contained in:
vabene1111 2022-05-31 21:52:59 +02:00
parent 007b7294d9
commit ded092ed23
13 changed files with 1282 additions and 56 deletions

View File

@ -9,7 +9,7 @@ from django.utils.translation import gettext as _
from rest_framework import permissions
from rest_framework.permissions import SAFE_METHODS
from cookbook.models import ShareLink, Recipe, UserPreference
from cookbook.models import ShareLink, Recipe, UserPreference, UserSpace
def get_allowed_groups(groups_required):
@ -53,7 +53,6 @@ def is_object_owner(user, obj):
Tests if a given user is the owner of a given object
test performed by checking user against the objects user
and create_by field (if exists)
superusers bypass all checks, unauthenticated users cannot own anything
:param user django auth user object
:param obj any object that should be tested
:return: true if user is owner of object, false otherwise
@ -66,11 +65,25 @@ def is_object_owner(user, obj):
return False
def is_space_owner(user, obj):
"""
Tests if a given user is the owner the space of a given object
:param user django auth user object
:param obj any object that should be tested
:return: true if user is owner of the objects space, false otherwise
"""
if not user.is_authenticated:
return False
try:
return obj.get_space().get_owner() == user
except Exception:
return False
def is_object_shared(user, obj):
"""
Tests if a given user is shared for a given object
test performed by checking user against the objects shared table
superusers bypass all checks, unauthenticated users cannot own anything
:param user django auth user object
:param obj any object that should be tested
:return: true if user is shared for object, false otherwise
@ -184,7 +197,7 @@ class CustomIsOwner(permissions.BasePermission):
verifies user has ownership over object
(either user or created_by or user is request user)
"""
message = _('You cannot interact with this object as it is not owned by you!') # noqa: E501
message = _('You cannot interact with this object as it is not owned by you!')
def has_permission(self, request, view):
return request.user.is_authenticated
@ -193,6 +206,20 @@ class CustomIsOwner(permissions.BasePermission):
return is_object_owner(request.user, obj)
class CustomIsSpaceOwner(permissions.BasePermission):
"""
Custom permission class for django rest framework views
verifies if the user is the owner of the space the object belongs to
"""
message = _('You cannot interact with this object as it is not owned by you!')
def has_permission(self, request, view):
return request.user.is_authenticated
def has_object_permission(self, request, view, obj):
return is_space_owner(request.user, obj)
# TODO function duplicate/too similar name
class CustomIsShared(permissions.BasePermission):
"""
@ -293,7 +320,19 @@ def above_space_user_limit(space):
:param space: Space to test for limits
:return: Tuple (True if above or equal limit else false, message)
"""
limit = space.max_users != 0 and UserPreference.objects.filter(space=space).count() > space.max_users
limit = space.max_users != 0 and UserSpace.objects.filter(space=space).count() > space.max_users
if limit:
return True, _('You have more users than allowed in your space.')
return False, ''
def switch_user_active_space(user, user_space):
"""
Switch the currently active space of a user by setting all spaces to inactive and activating the one passed
:param user: user to change active space for
:param user_space: user space object to activate
"""
if not user_space.active:
UserSpace.objects.filter(user=user).update(active=False) # make sure to deactivate all spaces for a user
user_space.active = True
user_space.save()

View File

@ -241,6 +241,9 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
food_inherit = models.ManyToManyField(FoodInheritField, blank=True)
show_facet_count = models.BooleanField(default=False)
def get_owner(self):
return self.created_by
def __str__(self):
return self.name

View File

@ -2,7 +2,7 @@ from datetime import timedelta
from decimal import Decimal
from gettext import gettext as _
from django.contrib.auth.models import User
from django.contrib.auth.models import User, Group
from django.db.models import Avg, Q, QuerySet, Sum
from django.urls import reverse
from django.utils import timezone
@ -20,7 +20,7 @@ from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Cu
RecipeBookEntry, RecipeImport, ShareLink, ShoppingList,
ShoppingListEntry, ShoppingListRecipe, Step, Storage, Supermarket,
SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, Unit,
UserFile, UserPreference, ViewLog)
UserFile, UserPreference, ViewLog, Space, UserSpace)
from cookbook.templatetags.custom_tags import markdown
from recipes.settings import MEDIA_URL, AWS_ENABLED
@ -123,6 +123,65 @@ class SpaceFilterSerializer(serializers.ListSerializer):
return super().to_representation(data)
class UserNameSerializer(WritableNestedModelSerializer):
username = serializers.SerializerMethodField('get_user_label')
def get_user_label(self, obj):
return obj.get_user_name()
class Meta:
list_serializer_class = SpaceFilterSerializer
model = User
fields = ('id', 'username')
class GroupSerializer(WritableNestedModelSerializer):
def create(self, validated_data):
raise ValidationError('Cannot create using this endpoint')
class Meta:
model = Group
fields = ('id', 'name')
class SpaceSerializer(serializers.ModelSerializer):
user_count = serializers.SerializerMethodField('get_user_count')
recipe_count = serializers.SerializerMethodField('get_recipe_count')
file_size_mb = serializers.SerializerMethodField('get_file_size_mb')
def get_user_count(self, obj):
return UserSpace.objects.filter(space=obj).count()
def get_recipe_count(self, obj):
return Recipe.objects.filter(space=obj).count()
def get_file_size_mb(self, obj):
try:
return UserFile.objects.filter(space=obj).aggregate(Sum('file_size_kb'))['file_size_kb__sum'] / 1000
except TypeError:
return 0
def create(self, validated_data):
raise ValidationError('Cannot create using this endpoint')
class Meta:
model = Space
fields = ('id', 'name', 'created_by', 'created_at', 'message', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing', 'demo', 'food_inherit', 'show_facet_count', 'user_count', 'recipe_count', 'file_size_mb',)
read_only_fields = ('id', 'created_by', 'created_at', 'message', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing', 'demo',)
class UserSpaceSerializer(serializers.ModelSerializer):
user = UserNameSerializer(read_only=True)
def create(self, validated_data):
raise ValidationError('Cannot create using this endpoint')
class Meta:
model = UserSpace
fields = ('id', 'user', 'space', 'groups', 'created_at', 'updated_at',)
read_only_fields = ('id', 'created_at', 'updated_at', 'space')
class SpacedModelSerializer(serializers.ModelSerializer):
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
@ -142,18 +201,6 @@ class MealTypeSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
read_only_fields = ('created_by',)
class UserNameSerializer(WritableNestedModelSerializer):
username = serializers.SerializerMethodField('get_user_label')
def get_user_label(self, obj):
return obj.get_user_name()
class Meta:
list_serializer_class = SpaceFilterSerializer
model = User
fields = ('id', 'username')
class FoodInheritFieldSerializer(WritableNestedModelSerializer):
name = serializers.CharField(allow_null=True, allow_blank=True, required=False)
field = serializers.CharField(allow_null=True, allow_blank=True, required=False)

View File

@ -346,7 +346,7 @@
{% message_of_the_day as message_of_the_day %}
{% if message_of_the_day %}
<div class="bg-warning" style=" width: 100%; text-align: center!important; color: #ffffff; padding: 8px">
<div class="bg-success" style=" width: 100%; text-align: center!important; color: #ffffff; padding: 8px">
{{ message_of_the_day }}
</div>
{% endif %}

View File

@ -0,0 +1,55 @@
{% extends "base.html" %}
{% load render_bundle from webpack_loader %}
{% load static %}
{% load i18n %}
{% load l10n %}
{% block title %}{% trans 'Search' %}{% endblock %}
{% block content %}
<div class="row">
<div class="col col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'view_space' %}">{% trans 'Space Settings' %}</a></li>
</ol>
</nav>
</div>
</div>
<div class="row">
<div class="col col-12">
<h3>
<span class="text-muted">{% trans 'Space:' %}</span> {{ request.space.name }}
<small>{% if HOSTED %} <a href="https://tandoor.dev/manage">{% trans 'Manage Subscription' %}</a>
{% endif %}</small>
</h3>
</div>
</div>
<div id="app">
<space-manage-view></space-manage-view>
</div>
{% endblock %}
{% block script %}
{% if debug %}
<script src="{% url 'js_reverse' %}"></script>
{% else %}
<script src="{% static 'django_js_reverse/reverse.js' %}"></script>
{% endif %}
<script type="application/javascript">
window.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
window.ACTIVE_SPACE_ID = {{ request.space.id }}
</script>
{% render_bundle 'space_manage_view' %}
{% endblock %}

View File

@ -25,6 +25,7 @@ router.register(r'food', api.FoodViewSet)
router.register(r'food-inherit-field', api.FoodInheritFieldViewSet)
router.register(r'import-log', api.ImportLogViewSet)
router.register(r'export-log', api.ExportLogViewSet)
router.register(r'group', api.GroupViewSet)
router.register(r'ingredient', api.IngredientViewSet)
router.register(r'keyword', api.KeywordViewSet)
router.register(r'meal-plan', api.MealPlanViewSet)
@ -35,6 +36,7 @@ router.register(r'recipe-book-entry', api.RecipeBookEntryViewSet)
router.register(r'shopping-list', api.ShoppingListViewSet)
router.register(r'shopping-list-entry', api.ShoppingListEntryViewSet)
router.register(r'shopping-list-recipe', api.ShoppingListRecipeViewSet)
router.register(r'space', api.SpaceViewSet)
router.register(r'step', api.StepViewSet)
router.register(r'storage', api.StorageViewSet)
router.register(r'supermarket', api.SupermarketViewSet)
@ -46,6 +48,7 @@ router.register(r'unit', api.UnitViewSet)
router.register(r'user-file', api.UserFileViewSet)
router.register(r'user-name', api.UserNameViewSet, basename='username')
router.register(r'user-preference', api.UserPreferenceViewSet)
router.register(r'user-space', api.UserSpaceViewSet)
router.register(r'view-log', api.ViewLogViewSet)
urlpatterns = [
@ -56,6 +59,7 @@ urlpatterns = [
name='change_space_member'),
path('no-group', views.no_groups, name='view_no_group'),
path('space-overview', views.space_overview, name='view_space_overview'),
path('space-manage/<int:space_id>', views.space_manage, name='view_space_manage'),
path('switch-space/<int:space_id>', views.switch_space, name='view_switch_space'),
path('no-perm', views.no_perm, name='view_no_perm'),
path('signup/<slug:token>', views.signup, name='view_signup'), # TODO deprecated with 0.16.2 remove at some point

View File

@ -11,7 +11,7 @@ from PIL import UnidentifiedImageError
from annoying.decorators import ajax_request
from annoying.functions import get_object_or_None
from django.contrib import messages
from django.contrib.auth.models import User
from django.contrib.auth.models import User, Group
from django.contrib.postgres.search import TrigramSimilarity
from django.core.exceptions import FieldError, ValidationError
from django.core.files import File
@ -29,26 +29,22 @@ from requests.exceptions import MissingSchema
from rest_framework import decorators, status, viewsets
from rest_framework.authtoken.models import Token
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.decorators import api_view, permission_classes, schema
from rest_framework.decorators import api_view, permission_classes
from rest_framework.exceptions import APIException, PermissionDenied
from rest_framework.generics import CreateAPIView
from rest_framework.pagination import PageNumberPagination
from rest_framework.parsers import MultiPartParser
from rest_framework.renderers import JSONRenderer, TemplateHTMLRenderer
from rest_framework.response import Response
from rest_framework.schemas import AutoSchema
from rest_framework.throttling import AnonRateThrottle
from rest_framework.views import APIView
from rest_framework.viewsets import ViewSetMixin
from treebeard.exceptions import InvalidMoveToDescendant, InvalidPosition, PathOverflow
from validators import ValidationFailure
from cookbook.helper.HelperFunctions import str2bool
from cookbook.helper.image_processing import handle_image
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest, CustomIsOwner,
CustomIsShare, CustomIsShared, CustomIsUser,
group_required)
group_required, CustomIsSpaceOwner)
from cookbook.helper.recipe_html_import import get_recipe_from_source
from cookbook.helper.recipe_search import RecipeFacet, RecipeSearch, old_search
from cookbook.helper.shopping_helper import RecipeShoppingEditor, shopping_helper
@ -57,7 +53,7 @@ from cookbook.models import (Automation, BookmarkletImport, CookLog, CustomFilte
Recipe, RecipeBook, RecipeBookEntry, ShareLink, ShoppingList,
ShoppingListEntry, ShoppingListRecipe, Step, Storage, Supermarket,
SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, Unit,
UserFile, UserPreference, ViewLog)
UserFile, UserPreference, ViewLog, Space, UserSpace)
from cookbook.provider.dropbox import Dropbox
from cookbook.provider.local import Local
from cookbook.provider.nextcloud import Nextcloud
@ -78,7 +74,7 @@ from cookbook.serializer import (AutomationSerializer, BookmarkletImportSerializ
SupermarketCategorySerializer, SupermarketSerializer,
SyncLogSerializer, SyncSerializer, UnitSerializer,
UserFileSerializer, UserNameSerializer, UserPreferenceSerializer,
ViewLogSerializer, IngredientSimpleSerializer, BookmarkletImportListSerializer, RecipeFromSourceSerializer)
ViewLogSerializer, IngredientSimpleSerializer, BookmarkletImportListSerializer, RecipeFromSourceSerializer, SpaceSerializer, UserSpaceSerializer, GroupSerializer)
from recipes import settings
@ -369,6 +365,33 @@ class UserNameViewSet(viewsets.ReadOnlyModelViewSet):
return queryset
class GroupViewSet(viewsets.ModelViewSet):
queryset = Group.objects.all()
serializer_class = GroupSerializer
permission_classes = [CustomIsAdmin]
http_method_names = ['get', ]
class SpaceViewSet(viewsets.ModelViewSet):
queryset = Space.objects
serializer_class = SpaceSerializer
permission_classes = [CustomIsOwner]
http_method_names = ['get', 'patch']
def get_queryset(self):
return self.queryset.filter(id=self.request.space.id)
class UserSpaceViewSet(viewsets.ModelViewSet):
queryset = UserSpace.objects
serializer_class = UserSpaceSerializer
permission_classes = [CustomIsSpaceOwner]
http_method_names = ['get', 'patch', 'delete']
def get_queryset(self):
return self.queryset.filter(space=self.request.space)
class UserPreferenceViewSet(viewsets.ModelViewSet):
queryset = UserPreference.objects
serializer_class = UserPreferenceSerializer

View File

@ -26,7 +26,7 @@ from cookbook.filters import RecipeFilter
from cookbook.forms import (CommentForm, Recipe, SearchPreferenceForm, ShoppingPreferenceForm,
SpaceCreateForm, SpaceJoinForm, SpacePreferenceForm, User,
UserCreateForm, UserNameForm, UserPreference, UserPreferenceForm)
from cookbook.helper.permission_helper import group_required, has_group_permission, share_link_valid
from cookbook.helper.permission_helper import group_required, has_group_permission, share_link_valid, switch_user_active_space
from cookbook.models import (Comment, CookLog, Food, InviteLink, Keyword,
MealPlan, RecipeImport, SearchFields, SearchPreference, ShareLink,
Space, Unit, ViewLog, UserSpace)
@ -144,9 +144,7 @@ def space_overview(request):
@login_required
def switch_space(request, space_id):
user_space = get_object_or_404(UserSpace, space=space_id, user=request.user)
UserSpace.objects.filter(user=request.user).update(active=False) # make sure to deactivate all spaces for a user
user_space.active = True
user_space.save()
switch_user_active_space(request.user, user_space)
return HttpResponseRedirect(reverse('index'))
@ -533,6 +531,12 @@ def signup(request, token):
return HttpResponseRedirect(reverse('view_invite', args=[token]))
@group_required('admin')
def space_manage(request, space_id):
user_space = get_object_or_404(UserSpace, space=space_id, user=request.user)
switch_user_active_space(request.user, user_space)
return render(request, 'space_manage.html', {})
@group_required('admin')
def space(request):
space_users = UserSpace.objects.filter(space=request.space).all()

View File

@ -0,0 +1,90 @@
<template>
<div id="app">
<div class="row mt-2">
<div class="col col-12">
<div v-if="space !== undefined">
Recipes {{ space.recipe_count }} / {{ space.max_recipes }}
Users {{ space.user_count }} / {{ space.max_users }}
Files {{ space.file_size_mb }} / {{ space.max_file_storage_mb }}
</div>
</div>
</div>
<div class="row mt-2">
<div class="col col-12">
<div v-if="user_spaces !== undefined">
<table class="table">
<thead>
<tr>
<th>{{ $t('User') }}</th>
<th>{{ $t('Groups') }}</th>
<th></th>
</tr>
</thead>
<tr v-for="us in user_spaces" :key="us.id">
<td>{{ us.user.username }}</td>
<td>
<generic-multiselect
class="input-group-text m-0 p-0"
@change="us.groups = $event.val"
label="name"
:initial_selection="us.groups"
:model="Models.GROUP"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
:limit="10"
:multiple="true"
/>
</td>
<td>
<button @click="alert('')">{{ $t('Delete') }}</button>
</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</template>
<script>
import Vue from "vue"
import {BootstrapVue} from "bootstrap-vue"
import "bootstrap-vue/dist/bootstrap-vue.css"
import {ApiMixin, ResolveUrlMixin, ToastMixin} from "@/utils/utils"
import {ApiApiFactory} from "@/utils/openapi/api.ts"
import GenericMultiselect from "@/components/GenericMultiselect";
Vue.use(BootstrapVue)
export default {
name: "SupermarketView",
mixins: [ResolveUrlMixin, ToastMixin, ApiMixin],
components: {GenericMultiselect},
data() {
return {
space: undefined,
user_spaces: []
}
},
mounted() {
this.$i18n.locale = window.CUSTOM_LOCALE
let apiFactory = new ApiApiFactory()
apiFactory.retrieveSpace(window.ACTIVE_SPACE_ID).then(r => {
this.space = r.data
})
apiFactory.listUserSpaces().then(r => {
this.user_spaces = r.data
})
},
methods: {},
}
</script>
<style></style>

View File

@ -0,0 +1,18 @@
import Vue from 'vue'
import App from './SpaceManageView.vue'
import i18n from '@/i18n'
Vue.config.productionTip = false
// TODO move this and other default stuff to centralized JS file (verify nothing breaks)
let publicPath = localStorage.STATIC_URL + 'vue/'
if (process.env.NODE_ENV === 'development') {
publicPath = 'http://localhost:8080/'
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
new Vue({
i18n,
render: h => h(App),
}).$mount('#app')

View File

@ -23,7 +23,7 @@ export class Models {
false: undefined,
},
},
tree: { default: undefined },
tree: {default: undefined},
},
},
delete: {
@ -50,7 +50,7 @@ export class Models {
type: "lookup",
field: "target",
list: "self",
sticky_options: [{ id: 0, name: "tree_root" }],
sticky_options: [{id: 0, name: "tree_root"}],
},
},
},
@ -71,7 +71,7 @@ export class Models {
food_onhand: true,
shopping: true,
},
tags: [{ field: "supermarket_category", label: "name", color: "info" }],
tags: [{field: "supermarket_category", label: "name", color: "info"}],
// REQUIRED: unordered array of fields that can be set during create
create: {
// if not defined partialUpdate will use the same parameters, prepending 'id'
@ -159,7 +159,7 @@ export class Models {
field: "substitute_siblings",
label: "substitute_siblings", // form.label always translated in utils.getForm()
help_text: "substitute_siblings_help", // form.help_text always translated
condition: { field: "parent", value: true, condition: "field_exists" },
condition: {field: "parent", value: true, condition: "field_exists"},
},
substitute_children: {
form_field: true,
@ -168,7 +168,7 @@ export class Models {
field: "substitute_children",
label: "substitute_children",
help_text: "substitute_children_help",
condition: { field: "numchild", value: 0, condition: "gt" },
condition: {field: "numchild", value: 0, condition: "gt"},
},
inherit_fields: {
form_field: true,
@ -178,7 +178,7 @@ export class Models {
field: "inherit_fields",
list: "FOOD_INHERIT_FIELDS",
label: "InheritFields",
condition: { field: "food_children_exist", value: true, condition: "preference_equals" },
condition: {field: "food_children_exist", value: true, condition: "preference_equals"},
help_text: "InheritFields_help",
},
child_inherit_fields: {
@ -189,7 +189,7 @@ export class Models {
field: "child_inherit_fields",
list: "FOOD_INHERIT_FIELDS",
label: "ChildInheritFields", // form.label always translated in utils.getForm()
condition: { field: "numchild", value: 0, condition: "gt" },
condition: {field: "numchild", value: 0, condition: "gt"},
help_text: "ChildInheritFields_help", // form.help_text always translated
},
reset_inherit: {
@ -199,7 +199,7 @@ export class Models {
field: "reset_inherit",
label: "reset_children",
help_text: "reset_children_help",
condition: { field: "numchild", value: 0, condition: "gt" },
condition: {field: "numchild", value: 0, condition: "gt"},
},
form_function: "FoodCreateDefault",
},
@ -399,7 +399,7 @@ export class Models {
static SUPERMARKET = {
name: "Supermarket",
apiName: "Supermarket",
ordered_tags: [{ field: "category_to_supermarket", label: "category::name", color: "info" }],
ordered_tags: [{field: "category_to_supermarket", label: "category::name", color: "info"}],
create: {
params: [["name", "description", "category_to_supermarket"]],
form: {
@ -469,9 +469,9 @@ export class Models {
form_field: true,
type: "choice",
options: [
{ value: "FOOD_ALIAS", text: "Food_Alias" },
{ value: "UNIT_ALIAS", text: "Unit_Alias" },
{ value: "KEYWORD_ALIAS", text: "Keyword_Alias" },
{value: "FOOD_ALIAS", text: "Food_Alias"},
{value: "UNIT_ALIAS", text: "Unit_Alias"},
{value: "KEYWORD_ALIAS", text: "Keyword_Alias"},
],
field: "type",
label: "Type",
@ -669,6 +669,12 @@ export class Models {
paginated: false,
}
static GROUP = {
name: "Group",
apiName: "Group",
paginated: false,
}
static STEP = {
name: "Step",
apiName: "Step",
@ -694,7 +700,7 @@ export class Actions {
},
],
},
ok_label: { function: "translate", phrase: "Save" },
ok_label: {function: "translate", phrase: "Save"},
},
}
static UPDATE = {
@ -729,7 +735,7 @@ export class Actions {
},
],
},
ok_label: { function: "translate", phrase: "Delete" },
ok_label: {function: "translate", phrase: "Delete"},
instruction: {
form_field: true,
type: "instruction",
@ -756,17 +762,17 @@ export class Actions {
suffix: "s",
params: ["query", "page", "pageSize", "options"],
config: {
query: { default: undefined },
page: { default: 1 },
pageSize: { default: 25 },
query: {default: undefined},
page: {default: 1},
pageSize: {default: 25},
},
}
static MERGE = {
function: "merge",
params: ["source", "target"],
config: {
source: { type: "string" },
target: { type: "string" },
source: {type: "string"},
target: {type: "string"},
},
form: {
title: {
@ -781,7 +787,7 @@ export class Actions {
},
],
},
ok_label: { function: "translate", phrase: "Merge" },
ok_label: {function: "translate", phrase: "Merge"},
instruction: {
form_field: true,
type: "instruction",
@ -815,8 +821,8 @@ export class Actions {
function: "move",
params: ["source", "target"],
config: {
source: { type: "string" },
target: { type: "string" },
source: {type: "string"},
target: {type: "string"},
},
form: {
title: {
@ -831,7 +837,7 @@ export class Actions {
},
],
},
ok_label: { function: "translate", phrase: "Move" },
ok_label: {function: "translate", phrase: "Move"},
instruction: {
form_field: true,
type: "instruction",

File diff suppressed because it is too large Load Diff

View File

@ -57,6 +57,10 @@ const pages = {
entry: "./src/apps/ShoppingListView/main.js",
chunks: ["chunk-vendors"],
},
space_manage_view: {
entry: "./src/apps/SpaceManageView/main.js",
chunks: ["chunk-vendors"],
},
}
module.exports = {