Merge pull request #1086 from smilerz/generic_modal_v2
generic modal refactored
This commit is contained in:
commit
adc65baf9c
@ -2,25 +2,30 @@ import operator
|
|||||||
import pathlib
|
import pathlib
|
||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
|
from collections import OrderedDict
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
from annoying.fields import AutoOneToOneField
|
from annoying.fields import AutoOneToOneField
|
||||||
from django.contrib import auth
|
from django.contrib import auth
|
||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth.models import Group, User
|
||||||
from django.contrib.postgres.indexes import GinIndex
|
from django.contrib.postgres.indexes import GinIndex
|
||||||
from django.contrib.postgres.search import SearchVectorField
|
from django.contrib.postgres.search import SearchVectorField
|
||||||
from django.core.files.uploadedfile import UploadedFile, InMemoryUploadedFile
|
from django.core.files.uploadedfile import InMemoryUploadedFile, UploadedFile
|
||||||
from django.core.validators import MinLengthValidator
|
from django.core.validators import MinLengthValidator
|
||||||
from django.db import models, IntegrityError
|
from django.db import IntegrityError, models
|
||||||
from django.db.models import Index, ProtectedError
|
from django.db.models import Index, ProtectedError, Q, Subquery
|
||||||
|
from django.db.models.fields.related import ManyToManyField
|
||||||
|
from django.db.models.functions import Substr
|
||||||
|
from django.db.transaction import atomic
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from treebeard.mp_tree import MP_Node, MP_NodeManager
|
|
||||||
from django_scopes import ScopedManager, scopes_disabled
|
|
||||||
from django_prometheus.models import ExportModelOperationsMixin
|
from django_prometheus.models import ExportModelOperationsMixin
|
||||||
from recipes.settings import (COMMENT_PREF_DEFAULT, FRACTION_PREF_DEFAULT,
|
from django_scopes import ScopedManager, scopes_disabled
|
||||||
KJ_PREF_DEFAULT, STICKY_NAV_PREF_DEFAULT,
|
from treebeard.mp_tree import MP_Node, MP_NodeManager
|
||||||
SORT_TREE_BY_NAME)
|
|
||||||
|
from recipes.settings import (COMMENT_PREF_DEFAULT, FRACTION_PREF_DEFAULT, KJ_PREF_DEFAULT,
|
||||||
|
SORT_TREE_BY_NAME, STICKY_NAV_PREF_DEFAULT)
|
||||||
|
|
||||||
|
|
||||||
def get_user_name(self):
|
def get_user_name(self):
|
||||||
@ -38,15 +43,26 @@ def get_model_name(model):
|
|||||||
|
|
||||||
|
|
||||||
class TreeManager(MP_NodeManager):
|
class TreeManager(MP_NodeManager):
|
||||||
|
def create(self, *args, **kwargs):
|
||||||
|
return self.get_or_create(*args, **kwargs)[0]
|
||||||
|
|
||||||
# model.Manager get_or_create() is not compatible with MP_Tree
|
# model.Manager get_or_create() is not compatible with MP_Tree
|
||||||
def get_or_create(self, **kwargs):
|
def get_or_create(self, *args, **kwargs):
|
||||||
kwargs['name'] = kwargs['name'].strip()
|
kwargs['name'] = kwargs['name'].strip()
|
||||||
try:
|
try:
|
||||||
return self.get(name__exact=kwargs['name'], space=kwargs['space']), False
|
return self.get(name__exact=kwargs['name'], space=kwargs['space']), False
|
||||||
except self.model.DoesNotExist:
|
except self.model.DoesNotExist:
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
try:
|
try:
|
||||||
return self.model.add_root(**kwargs), True
|
# ManyToMany fields can't be set this way, so pop them out to save for later
|
||||||
|
fields = [field.name for field in self.model._meta.get_fields() if issubclass(type(field), ManyToManyField)]
|
||||||
|
many_to_many = {field: kwargs.pop(field) for field in list(kwargs) if field in fields}
|
||||||
|
obj = self.model.add_root(**kwargs)
|
||||||
|
for field in many_to_many:
|
||||||
|
field_model = getattr(obj, field).model
|
||||||
|
for related_obj in many_to_many[field]:
|
||||||
|
getattr(obj, field).add(field_model.objects.get(**dict(related_obj)))
|
||||||
|
return obj, True
|
||||||
except IntegrityError as e:
|
except IntegrityError as e:
|
||||||
if 'Key (path)' in e.args[0]:
|
if 'Key (path)' in e.args[0]:
|
||||||
self.model.fix_tree(fix_paths=True)
|
self.model.fix_tree(fix_paths=True)
|
||||||
|
@ -2,78 +2,29 @@ from rest_framework.schemas.openapi import AutoSchema
|
|||||||
from rest_framework.schemas.utils import is_list_view
|
from rest_framework.schemas.utils import is_list_view
|
||||||
|
|
||||||
|
|
||||||
# TODO move to separate class to cleanup
|
class QueryParam(object):
|
||||||
class RecipeSchema(AutoSchema):
|
def __init__(self, name, description=None, qtype='string', required=False):
|
||||||
|
self.name = name
|
||||||
|
self.description = description
|
||||||
|
self.qtype = qtype
|
||||||
|
self.required = required
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.name}, {self.qtype}, {self.description}'
|
||||||
|
|
||||||
|
|
||||||
|
class QueryParamAutoSchema(AutoSchema):
|
||||||
def get_path_parameters(self, path, method):
|
def get_path_parameters(self, path, method):
|
||||||
if not is_list_view(path, method, self.view):
|
if not is_list_view(path, method, self.view):
|
||||||
return super(RecipeSchema, self).get_path_parameters(path, method)
|
return super().get_path_parameters(path, method)
|
||||||
|
|
||||||
parameters = super().get_path_parameters(path, method)
|
parameters = super().get_path_parameters(path, method)
|
||||||
parameters.append({
|
for q in self.view.query_params:
|
||||||
"name": 'query', "in": "query", "required": False,
|
parameters.append({
|
||||||
"description": 'Query string matched (fuzzy) against recipe name. In the future also fulltext search.',
|
"name": q.name, "in": "query", "required": q.required,
|
||||||
'schema': {'type': 'string', },
|
"description": q.description,
|
||||||
})
|
'schema': {'type': q.qtype, },
|
||||||
parameters.append({
|
})
|
||||||
"name": 'keywords', "in": "query", "required": False,
|
|
||||||
"description": 'Id of keyword a recipe should have. For multiple repeat parameter.',
|
|
||||||
'schema': {'type': 'string', },
|
|
||||||
})
|
|
||||||
parameters.append({
|
|
||||||
"name": 'foods', "in": "query", "required": False,
|
|
||||||
"description": 'Id of food a recipe should have. For multiple repeat parameter.',
|
|
||||||
'schema': {'type': 'string', },
|
|
||||||
})
|
|
||||||
parameters.append({
|
|
||||||
"name": 'units', "in": "query", "required": False,
|
|
||||||
"description": 'Id of unit a recipe should have.',
|
|
||||||
'schema': {'type': 'int', },
|
|
||||||
})
|
|
||||||
parameters.append({
|
|
||||||
"name": 'rating', "in": "query", "required": False,
|
|
||||||
"description": 'Id of unit a recipe should have.',
|
|
||||||
'schema': {'type': 'int', },
|
|
||||||
})
|
|
||||||
parameters.append({
|
|
||||||
"name": 'books', "in": "query", "required": False,
|
|
||||||
"description": 'Id of book a recipe should have. For multiple repeat parameter.',
|
|
||||||
'schema': {'type': 'string', },
|
|
||||||
})
|
|
||||||
parameters.append({
|
|
||||||
"name": 'steps', "in": "query", "required": False,
|
|
||||||
"description": 'Id of a step a recipe should have. For multiple repeat parameter.',
|
|
||||||
'schema': {'type': 'string', },
|
|
||||||
})
|
|
||||||
parameters.append({
|
|
||||||
"name": 'keywords_or', "in": "query", "required": False,
|
|
||||||
"description": 'If recipe should have all (AND) or any (OR) of the provided keywords.',
|
|
||||||
'schema': {'type': 'string', },
|
|
||||||
})
|
|
||||||
parameters.append({
|
|
||||||
"name": 'foods_or', "in": "query", "required": False,
|
|
||||||
"description": 'If recipe should have all (AND) or any (OR) any of the provided foods.',
|
|
||||||
'schema': {'type': 'string', },
|
|
||||||
})
|
|
||||||
parameters.append({
|
|
||||||
"name": 'books_or', "in": "query", "required": False,
|
|
||||||
"description": 'If recipe should be in all (AND) or any (OR) any of the provided books.',
|
|
||||||
'schema': {'type': 'string', },
|
|
||||||
})
|
|
||||||
parameters.append({
|
|
||||||
"name": 'internal', "in": "query", "required": False,
|
|
||||||
"description": 'true or false. If only internal recipes should be returned or not.',
|
|
||||||
'schema': {'type': 'string', },
|
|
||||||
})
|
|
||||||
parameters.append({
|
|
||||||
"name": 'random', "in": "query", "required": False,
|
|
||||||
"description": 'true or false. returns the results in randomized order.',
|
|
||||||
'schema': {'type': 'string', },
|
|
||||||
})
|
|
||||||
parameters.append({
|
|
||||||
"name": 'new', "in": "query", "required": False,
|
|
||||||
"description": 'true or false. returns new results first in search results',
|
|
||||||
'schema': {'type': 'string', },
|
|
||||||
})
|
|
||||||
return parameters
|
return parameters
|
||||||
|
|
||||||
|
|
||||||
@ -118,15 +69,15 @@ class FilterSchema(AutoSchema):
|
|||||||
return parameters
|
return parameters
|
||||||
|
|
||||||
|
|
||||||
class QueryOnlySchema(AutoSchema):
|
# class QueryOnlySchema(AutoSchema):
|
||||||
def get_path_parameters(self, path, method):
|
# def get_path_parameters(self, path, method):
|
||||||
if not is_list_view(path, method, self.view):
|
# if not is_list_view(path, method, self.view):
|
||||||
return super(QueryOnlySchema, self).get_path_parameters(path, method)
|
# return super(QueryOnlySchema, self).get_path_parameters(path, method)
|
||||||
|
|
||||||
parameters = super().get_path_parameters(path, method)
|
# parameters = super().get_path_parameters(path, method)
|
||||||
parameters.append({
|
# parameters.append({
|
||||||
"name": 'query', "in": "query", "required": False,
|
# "name": 'query', "in": "query", "required": False,
|
||||||
"description": 'Query string matched (fuzzy) against object name.',
|
# "description": 'Query string matched (fuzzy) against object name.',
|
||||||
'schema': {'type': 'string', },
|
# 'schema': {'type': 'string', },
|
||||||
})
|
# })
|
||||||
return parameters
|
# return parameters
|
||||||
|
@ -336,6 +336,10 @@
|
|||||||
{% block content_fluid %}
|
{% block content_fluid %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% user_prefs request as prefs%}
|
||||||
|
{{ prefs|json_script:'user_preference' }}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
|
@ -28,10 +28,10 @@
|
|||||||
{% trans 'Account' %}</a>
|
{% trans 'Account' %}</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link {% if active_tab == 'prefernces' %} active {% endif %}" id="preferences-tab"
|
<a class="nav-link {% if active_tab == 'preferences' %} active {% endif %}" id="preferences-tab"
|
||||||
data-toggle="tab" href="#preferences" role="tab"
|
data-toggle="tab" href="#preferences" role="tab"
|
||||||
aria-controls="preferences"
|
aria-controls="preferences"
|
||||||
aria-selected="{% if active_tab == 'prefernces' %} 'true' {% else %} 'false' {% endif %}">
|
aria-selected="{% if active_tab == 'preferences' %} 'true' {% else %} 'false' {% endif %}">
|
||||||
{% trans 'Preferences' %}</a>
|
{% trans 'Preferences' %}</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
@ -225,4 +225,4 @@
|
|||||||
window.location.hash = e.target.hash;
|
window.location.hash = e.target.hash;
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -28,13 +28,6 @@
|
|||||||
<span class="col col-md-9">
|
<span class="col col-md-9">
|
||||||
<h2>{% trans 'Shopping List' %}</h2>
|
<h2>{% trans 'Shopping List' %}</h2>
|
||||||
</span>
|
</span>
|
||||||
<span class="col-md-3">
|
|
||||||
<a href="{% url 'view_shopping_new' %}" class="float-right">
|
|
||||||
<button class="btn btn-outline-secondary shadow-none">
|
|
||||||
<i class="fas fa-star"></i> {% trans 'Try the new shopping list' %}
|
|
||||||
</button>
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
<div class="col col-mdd-3 text-right">
|
<div class="col col-mdd-3 text-right">
|
||||||
<b-form-checkbox switch size="lg" v-model="edit_mode"
|
<b-form-checkbox switch size="lg" v-model="edit_mode"
|
||||||
@change="$forceUpdate()">{% trans 'Edit' %}</b-form-checkbox>
|
@change="$forceUpdate()">{% trans 'Edit' %}</b-form-checkbox>
|
||||||
@ -977,4 +970,4 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% comment %} TODO: refactor to be Vue app {% endcomment %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load custom_tags %}
|
{% load custom_tags %}
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
|
import re
|
||||||
|
from gettext import gettext as _
|
||||||
|
|
||||||
import bleach
|
import bleach
|
||||||
import markdown as md
|
import markdown as md
|
||||||
import re
|
|
||||||
from bleach_allowlist import markdown_attrs, markdown_tags
|
from bleach_allowlist import markdown_attrs, markdown_tags
|
||||||
from cookbook.helper.mdx_attributes import MarkdownFormatExtension
|
|
||||||
from cookbook.helper.mdx_urlize import UrlizeExtension
|
|
||||||
from cookbook.models import Space, get_model_name
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.db.models import Avg
|
from django.db.models import Avg
|
||||||
from django.templatetags.static import static
|
from django.templatetags.static import static
|
||||||
from django.urls import NoReverseMatch, reverse
|
from django.urls import NoReverseMatch, reverse
|
||||||
from recipes import settings
|
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
from gettext import gettext as _
|
|
||||||
|
from cookbook.helper.mdx_attributes import MarkdownFormatExtension
|
||||||
|
from cookbook.helper.mdx_urlize import UrlizeExtension
|
||||||
|
from cookbook.models import Space, get_model_name
|
||||||
|
from recipes import settings
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
@ -124,10 +126,10 @@ def markdown_link():
|
|||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
def bookmarklet(request):
|
def bookmarklet(request):
|
||||||
if request.is_secure():
|
if request.is_secure():
|
||||||
prefix = "https://"
|
protocol = "https://"
|
||||||
else:
|
else:
|
||||||
prefix = "http://"
|
protocol = "http://"
|
||||||
server = prefix + request.get_host()
|
server = protocol + request.get_host()
|
||||||
prefix = settings.JS_REVERSE_SCRIPT_PREFIX
|
prefix = settings.JS_REVERSE_SCRIPT_PREFIX
|
||||||
# TODO is it safe to store the token in clear text in a bookmark?
|
# TODO is it safe to store the token in clear text in a bookmark?
|
||||||
if (api_token := Token.objects.filter(user=request.user).first()) is None:
|
if (api_token := Token.objects.filter(user=request.user).first()) is None:
|
||||||
@ -155,3 +157,13 @@ def base_path(request, path_type):
|
|||||||
return request.META.get('HTTP_X_SCRIPT_NAME', '')
|
return request.META.get('HTTP_X_SCRIPT_NAME', '')
|
||||||
elif path_type == 'static_base':
|
elif path_type == 'static_base':
|
||||||
return static('vue/manifest.json').replace('vue/manifest.json', '')
|
return static('vue/manifest.json').replace('vue/manifest.json', '')
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def user_prefs(request):
|
||||||
|
from cookbook.serializer import \
|
||||||
|
UserPreferenceSerializer # putting it with imports caused circular execution
|
||||||
|
try:
|
||||||
|
return UserPreferenceSerializer(request.user.userpreference).data
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.db.models import Subquery, OuterRef
|
from django.db.models import OuterRef, Subquery
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from cookbook.models import Step, Ingredient
|
from cookbook.models import Ingredient, Step
|
||||||
|
|
||||||
LIST_URL = 'api:step-list'
|
LIST_URL = 'api:step-list'
|
||||||
DETAIL_URL = 'api:step-detail'
|
DETAIL_URL = 'api:step-detail'
|
||||||
@ -34,7 +34,7 @@ def test_list_space(recipe_1_s1, u1_s1, u1_s2, space_2):
|
|||||||
|
|
||||||
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)['results']) == 0
|
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)['results']) == 0
|
||||||
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)['results']) == 2
|
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)['results']) == 2
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("arg", [
|
@pytest.mark.parametrize("arg", [
|
||||||
['a_u', 403],
|
['a_u', 403],
|
||||||
|
@ -36,7 +36,7 @@ from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest, Cus
|
|||||||
CustomIsShare, CustomIsShared, CustomIsUser,
|
CustomIsShare, CustomIsShared, CustomIsUser,
|
||||||
group_required)
|
group_required)
|
||||||
from cookbook.helper.recipe_html_import import get_recipe_from_source
|
from cookbook.helper.recipe_html_import import get_recipe_from_source
|
||||||
from cookbook.helper.recipe_search import get_facet, old_search, search_recipes
|
from cookbook.helper.recipe_search import get_facet, search_recipes
|
||||||
from cookbook.helper.recipe_url_import import get_from_scraper
|
from cookbook.helper.recipe_url_import import get_from_scraper
|
||||||
from cookbook.models import (Automation, BookmarkletImport, CookLog, Food, ImportLog, Ingredient,
|
from cookbook.models import (Automation, BookmarkletImport, CookLog, Food, ImportLog, Ingredient,
|
||||||
Keyword, MealPlan, MealType, Recipe, RecipeBook, RecipeBookEntry,
|
Keyword, MealPlan, MealType, Recipe, RecipeBook, RecipeBookEntry,
|
||||||
@ -46,7 +46,9 @@ from cookbook.models import (Automation, BookmarkletImport, CookLog, Food, Impor
|
|||||||
from cookbook.provider.dropbox import Dropbox
|
from cookbook.provider.dropbox import Dropbox
|
||||||
from cookbook.provider.local import Local
|
from cookbook.provider.local import Local
|
||||||
from cookbook.provider.nextcloud import Nextcloud
|
from cookbook.provider.nextcloud import Nextcloud
|
||||||
from cookbook.schemas import FilterSchema, QueryOnlySchema, RecipeSchema, TreeSchema
|
|
||||||
|
from cookbook.schemas import FilterSchema, QueryOnlySchema, RecipeSchema, TreeSchema,QueryParamAutoSchema
|
||||||
|
|
||||||
from cookbook.serializer import (AutomationSerializer, BookmarkletImportSerializer,
|
from cookbook.serializer import (AutomationSerializer, BookmarkletImportSerializer,
|
||||||
CookLogSerializer, FoodSerializer, ImportLogSerializer,
|
CookLogSerializer, FoodSerializer, ImportLogSerializer,
|
||||||
IngredientSerializer, KeywordSerializer, MealPlanSerializer,
|
IngredientSerializer, KeywordSerializer, MealPlanSerializer,
|
||||||
@ -216,7 +218,7 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin):
|
|||||||
if root.isnumeric():
|
if root.isnumeric():
|
||||||
try:
|
try:
|
||||||
root = int(root)
|
root = int(root)
|
||||||
except self.model.DoesNotExist:
|
except ValueError:
|
||||||
self.queryset = self.model.objects.none()
|
self.queryset = self.model.objects.none()
|
||||||
if root == 0:
|
if root == 0:
|
||||||
self.queryset = self.model.get_root_nodes()
|
self.queryset = self.model.get_root_nodes()
|
||||||
@ -244,7 +246,7 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin):
|
|||||||
try:
|
try:
|
||||||
child = self.model.objects.get(pk=pk, space=self.request.space)
|
child = self.model.objects.get(pk=pk, space=self.request.space)
|
||||||
except (self.model.DoesNotExist):
|
except (self.model.DoesNotExist):
|
||||||
content = {'error': True, 'msg': _(f'No {self.basename} with id {child} exists')}
|
content = {'error': True, 'msg': _(f'No {self.basename} with id {pk} exists')}
|
||||||
return Response(content, status=status.HTTP_404_NOT_FOUND)
|
return Response(content, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
parent = int(parent)
|
parent = int(parent)
|
||||||
@ -273,7 +275,7 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin):
|
|||||||
child.move(parent, f'{node_location}-child')
|
child.move(parent, f'{node_location}-child')
|
||||||
content = {'msg': _(f'{child.name} was moved successfully to parent {parent.name}')}
|
content = {'msg': _(f'{child.name} was moved successfully to parent {parent.name}')}
|
||||||
return Response(content, status=status.HTTP_200_OK)
|
return Response(content, status=status.HTTP_200_OK)
|
||||||
except (PathOverflow, InvalidMoveToDescendant, InvalidPosition):
|
except (PathOverflow, InvalidMoveToDescendant, InvalidPosition) as e:
|
||||||
content = {'error': True, 'msg': _('An error occurred attempting to move ') + child.name}
|
content = {'error': True, 'msg': _('An error occurred attempting to move ') + child.name}
|
||||||
return Response(content, status=status.HTTP_400_BAD_REQUEST)
|
return Response(content, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
@ -497,15 +499,20 @@ class StepViewSet(viewsets.ModelViewSet):
|
|||||||
serializer_class = StepSerializer
|
serializer_class = StepSerializer
|
||||||
permission_classes = [CustomIsUser]
|
permission_classes = [CustomIsUser]
|
||||||
pagination_class = DefaultPagination
|
pagination_class = DefaultPagination
|
||||||
schema = QueryOnlySchema()
|
query_params = [
|
||||||
|
QueryParam(name='recipe', description=_('ID of recipe a step is part of. For multiple repeat parameter.'), qtype='int'),
|
||||||
|
QueryParam(name='query', description=_('Query string matched (fuzzy) against object name.'), qtype='string'),
|
||||||
|
]
|
||||||
|
schema = QueryParamAutoSchema()
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = self.queryset.filter(recipe__space=self.request.space)
|
recipes = self.request.query_params.getlist('recipe', [])
|
||||||
|
|
||||||
query = self.request.query_params.get('query', None)
|
query = self.request.query_params.get('query', None)
|
||||||
|
if len(recipes) > 0:
|
||||||
|
self.queryset = self.queryset.filter(recipe__in=recipes)
|
||||||
if query is not None:
|
if query is not None:
|
||||||
queryset = queryset.filter(Q(name__icontains=query) | Q(recipe__name__icontains=query))
|
queryset = queryset.filter(Q(name__icontains=query) | Q(recipe__name__icontains=query))
|
||||||
return queryset
|
return self.queryset.filter(recipe__space=self.request.space)
|
||||||
|
|
||||||
|
|
||||||
class RecipePagination(PageNumberPagination):
|
class RecipePagination(PageNumberPagination):
|
||||||
@ -533,8 +540,22 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
|||||||
# TODO split read and write permission for meal plan guest
|
# TODO split read and write permission for meal plan guest
|
||||||
permission_classes = [CustomIsShare | CustomIsGuest]
|
permission_classes = [CustomIsShare | CustomIsGuest]
|
||||||
pagination_class = RecipePagination
|
pagination_class = RecipePagination
|
||||||
|
# TODO the boolean params below (keywords_or through new) should be updated to boolean types with front end refactored accordingly
|
||||||
schema = RecipeSchema()
|
query_params = [
|
||||||
|
QueryParam(name='query', description=_('Query string matched (fuzzy) against recipe name. In the future also fulltext search.')),
|
||||||
|
QueryParam(name='keywords', description=_('ID of keyword a recipe should have. For multiple repeat parameter.'), qtype='int'),
|
||||||
|
QueryParam(name='foods', description=_('ID of food a recipe should have. For multiple repeat parameter.'), qtype='int'),
|
||||||
|
QueryParam(name='units', description=_('ID of unit a recipe should have.'), qtype='int'),
|
||||||
|
QueryParam(name='rating', description=_('Rating a recipe should have. [0 - 5]'), qtype='int'),
|
||||||
|
QueryParam(name='books', description=_('ID of book a recipe should be in. For multiple repeat parameter.')),
|
||||||
|
QueryParam(name='keywords_or', description=_('If recipe should have all (AND=''false'') or any (OR=''<b>true</b>'') of the provided keywords.')),
|
||||||
|
QueryParam(name='foods_or', description=_('If recipe should have all (AND=''false'') or any (OR=''<b>true</b>'') of the provided foods.')),
|
||||||
|
QueryParam(name='books_or', description=_('If recipe should be in all (AND=''false'') or any (OR=''<b>true</b>'') of the provided books.')),
|
||||||
|
QueryParam(name='internal', description=_('If only internal recipes should be returned. [''true''/''<b>false</b>'']')),
|
||||||
|
QueryParam(name='random', description=_('Returns the results in randomized order. [''true''/''<b>false</b>'']')),
|
||||||
|
QueryParam(name='new', description=_('Returns new results first in search results. [''true''/''<b>false</b>'']')),
|
||||||
|
]
|
||||||
|
schema = QueryParamAutoSchema()
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
share = self.request.query_params.get('share', None)
|
share = self.request.query_params.get('share', None)
|
||||||
@ -606,6 +627,15 @@ class ShoppingListEntryViewSet(viewsets.ModelViewSet):
|
|||||||
queryset = ShoppingListEntry.objects
|
queryset = ShoppingListEntry.objects
|
||||||
serializer_class = ShoppingListEntrySerializer
|
serializer_class = ShoppingListEntrySerializer
|
||||||
permission_classes = [CustomIsOwner | CustomIsShared]
|
permission_classes = [CustomIsOwner | CustomIsShared]
|
||||||
|
query_params = [
|
||||||
|
QueryParam(name='id', description=_('Returns the shopping list entry with a primary key of id. Multiple values allowed.'), qtype='int'),
|
||||||
|
QueryParam(
|
||||||
|
name='checked',
|
||||||
|
description=_('Filter shopping list entries on checked. [''true'', ''false'', ''both'', ''<b>recent</b>'']<br> - ''recent'' includes unchecked items and recently completed items.')
|
||||||
|
),
|
||||||
|
QueryParam(name='supermarket', description=_('Returns the shopping list entries sorted by supermarket category order.'), qtype='int'),
|
||||||
|
]
|
||||||
|
schema = QueryParamAutoSchema()
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.queryset.filter(
|
return self.queryset.filter(
|
||||||
@ -646,7 +676,7 @@ class ViewLogViewSet(viewsets.ModelViewSet):
|
|||||||
class CookLogViewSet(viewsets.ModelViewSet):
|
class CookLogViewSet(viewsets.ModelViewSet):
|
||||||
queryset = CookLog.objects
|
queryset = CookLog.objects
|
||||||
serializer_class = CookLogSerializer
|
serializer_class = CookLogSerializer
|
||||||
permission_classes = [CustomIsOwner] # CustomIsShared? since ratings are in the cooklog?
|
permission_classes = [CustomIsOwner]
|
||||||
pagination_class = DefaultPagination
|
pagination_class = DefaultPagination
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
@ -1,479 +1,477 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app" style="margin-bottom: 4vh" v-if="this_model">
|
<div id="app" style="margin-bottom: 4vh" v-if="this_model">
|
||||||
<generic-modal-form v-if="this_model"
|
<generic-modal-form v-if="this_model" :model="this_model" :action="this_action" :item1="this_item" :item2="this_target" :show="show_modal" @finish-action="finishAction" />
|
||||||
:model="this_model"
|
|
||||||
:action="this_action"
|
|
||||||
:item1="this_item"
|
|
||||||
:item2="this_target"
|
|
||||||
:show="show_modal"
|
|
||||||
@finish-action="finishAction"/>
|
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-2 d-none d-md-block"></div>
|
||||||
|
<div class="col-xl-8 col-12">
|
||||||
|
<div class="container-fluid d-flex flex-column flex-grow-1">
|
||||||
|
<!-- dynamically loaded header components -->
|
||||||
|
<div class="row" v-if="header_component_name !== ''">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<component :is="headerComponent"></component>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-2 d-none d-md-block">
|
<div class="col-md-9" style="margin-top: 1vh">
|
||||||
</div>
|
<h3>
|
||||||
<div class="col-xl-8 col-12">
|
<!-- <span><b-button variant="link" size="sm" class="text-dark shadow-none"><i class="fas fa-chevron-down"></i></b-button></span> -->
|
||||||
<div class="container-fluid d-flex flex-column flex-grow-1">
|
<model-menu />
|
||||||
|
<span>{{ this.this_model.name }}</span>
|
||||||
|
<span v-if="this_model.name !== 'Step'"
|
||||||
|
><b-button variant="link" @click="startAction({ action: 'new' })"><i class="fas fa-plus-circle fa-2x"></i></b-button></span
|
||||||
|
><!-- TODO add proper field to model config to determine if create should be available or not -->
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3" style="position: relative; margin-top: 1vh">
|
||||||
|
<b-form-checkbox
|
||||||
|
v-model="show_split"
|
||||||
|
name="check-button"
|
||||||
|
v-if="paginated"
|
||||||
|
class="shadow-none"
|
||||||
|
style="position: relative; top: 50%; transform: translateY(-50%)"
|
||||||
|
switch
|
||||||
|
>
|
||||||
|
{{ $t("show_split_screen") }}
|
||||||
|
</b-form-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- dynamically loaded header components -->
|
<div class="row">
|
||||||
<div class="row" v-if="header_component_name !== ''">
|
<div class="col" :class="{ 'col-md-6': show_split }">
|
||||||
<div class="col-md-12">
|
<!-- model isn't paginated and loads in one API call -->
|
||||||
<component :is="headerComponent"></component>
|
<div v-if="!paginated">
|
||||||
|
<generic-horizontal-card
|
||||||
|
v-for="i in items_left"
|
||||||
|
v-bind:key="i.id"
|
||||||
|
:item="i"
|
||||||
|
:model="this_model"
|
||||||
|
@item-action="startAction($event, 'left')"
|
||||||
|
@finish-action="finishAction"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- model is paginated and needs managed -->
|
||||||
|
<generic-infinite-cards v-if="paginated" :card_counts="left_counts" :scroll="show_split" @search="getItems($event, 'left')" @reset="resetList('left')">
|
||||||
|
<template v-slot:cards>
|
||||||
|
<generic-horizontal-card
|
||||||
|
v-for="i in items_left"
|
||||||
|
v-bind:key="i.id"
|
||||||
|
:item="i"
|
||||||
|
:model="this_model"
|
||||||
|
@item-action="startAction($event, 'left')"
|
||||||
|
@finish-action="finishAction"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</generic-infinite-cards>
|
||||||
|
</div>
|
||||||
|
<div class="col col-md-6" v-if="show_split">
|
||||||
|
<generic-infinite-cards
|
||||||
|
v-if="this_model"
|
||||||
|
:card_counts="right_counts"
|
||||||
|
:scroll="show_split"
|
||||||
|
@search="getItems($event, 'right')"
|
||||||
|
@reset="resetList('right')"
|
||||||
|
>
|
||||||
|
<template v-slot:cards>
|
||||||
|
<generic-horizontal-card
|
||||||
|
v-for="i in items_right"
|
||||||
|
v-bind:key="i.id"
|
||||||
|
:item="i"
|
||||||
|
:model="this_model"
|
||||||
|
@item-action="startAction($event, 'right')"
|
||||||
|
@finish-action="finishAction"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</generic-infinite-cards>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-9" style="margin-top: 1vh">
|
|
||||||
<h3>
|
|
||||||
<!-- <span><b-button variant="link" size="sm" class="text-dark shadow-none"><i class="fas fa-chevron-down"></i></b-button></span> -->
|
|
||||||
<model-menu/>
|
|
||||||
<span>{{ this.this_model.name }}</span>
|
|
||||||
<span v-if="this_model.name !== 'Step'"><b-button variant="link" @click="startAction({'action':'new'})"><i
|
|
||||||
class="fas fa-plus-circle fa-2x"></i></b-button></span><!-- TODO add proper field to model config to determine if create should be available or not -->
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3" style="position: relative; margin-top: 1vh">
|
|
||||||
<b-form-checkbox v-model="show_split" name="check-button" v-if="paginated"
|
|
||||||
class="shadow-none"
|
|
||||||
style="position:relative;top: 50%; transform: translateY(-50%);" switch>
|
|
||||||
{{ $t('show_split_screen') }}
|
|
||||||
</b-form-checkbox>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col" :class="{'col-md-6' : show_split}">
|
|
||||||
<!-- model isn't paginated and loads in one API call -->
|
|
||||||
<div v-if="!paginated">
|
|
||||||
<generic-horizontal-card v-for="i in items_left" v-bind:key="i.id"
|
|
||||||
:item=i
|
|
||||||
:model="this_model"
|
|
||||||
@item-action="startAction($event, 'left')"
|
|
||||||
@finish-action="finishAction"/>
|
|
||||||
</div>
|
|
||||||
<!-- model is paginated and needs managed -->
|
|
||||||
<generic-infinite-cards v-if="paginated"
|
|
||||||
:card_counts="left_counts"
|
|
||||||
:scroll="show_split"
|
|
||||||
@search="getItems($event, 'left')"
|
|
||||||
@reset="resetList('left')">
|
|
||||||
<template v-slot:cards>
|
|
||||||
<generic-horizontal-card
|
|
||||||
v-for="i in items_left" v-bind:key="i.id"
|
|
||||||
:item=i
|
|
||||||
:model="this_model"
|
|
||||||
@item-action="startAction($event, 'left')"
|
|
||||||
@finish-action="finishAction"/>
|
|
||||||
</template>
|
|
||||||
</generic-infinite-cards>
|
|
||||||
</div>
|
|
||||||
<div class="col col-md-6" v-if="show_split">
|
|
||||||
<generic-infinite-cards v-if="this_model"
|
|
||||||
:card_counts="right_counts"
|
|
||||||
:scroll="show_split"
|
|
||||||
@search="getItems($event, 'right')"
|
|
||||||
@reset="resetList('right')">
|
|
||||||
<template v-slot:cards>
|
|
||||||
<generic-horizontal-card
|
|
||||||
v-for="i in items_right" v-bind:key="i.id"
|
|
||||||
:item=i
|
|
||||||
:model="this_model"
|
|
||||||
@item-action="startAction($event, 'right')"
|
|
||||||
@finish-action="finishAction"/>
|
|
||||||
</template>
|
|
||||||
</generic-infinite-cards>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Vue from "vue"
|
||||||
|
import { BootstrapVue } from "bootstrap-vue"
|
||||||
|
|
||||||
import Vue from 'vue'
|
import "bootstrap-vue/dist/bootstrap-vue.css"
|
||||||
import {BootstrapVue} from 'bootstrap-vue'
|
|
||||||
|
|
||||||
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
import { CardMixin, ApiMixin, getConfig } from "@/utils/utils"
|
||||||
|
import { StandardToasts, ToastMixin } from "@/utils/utils"
|
||||||
|
|
||||||
import {CardMixin, ApiMixin, getConfig} from "@/utils/utils";
|
import GenericInfiniteCards from "@/components/GenericInfiniteCards"
|
||||||
import {StandardToasts, ToastMixin} from "@/utils/utils";
|
import GenericHorizontalCard from "@/components/GenericHorizontalCard"
|
||||||
|
import GenericModalForm from "@/components/Modals/GenericModalForm"
|
||||||
import GenericInfiniteCards from "@/components/GenericInfiniteCards";
|
import ModelMenu from "@/components/ModelMenu"
|
||||||
import GenericHorizontalCard from "@/components/GenericHorizontalCard";
|
import { ApiApiFactory } from "@/utils/openapi/api"
|
||||||
import GenericModalForm from "@/components/Modals/GenericModalForm";
|
|
||||||
import ModelMenu from "@/components/ModelMenu";
|
|
||||||
import {ApiApiFactory} from "@/utils/openapi/api";
|
|
||||||
//import StorageQuota from "@/components/StorageQuota";
|
//import StorageQuota from "@/components/StorageQuota";
|
||||||
|
|
||||||
Vue.use(BootstrapVue)
|
Vue.use(BootstrapVue)
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
// TODO ApiGenerator doesn't capture and share error information - would be nice to share error details when available
|
// TODO ApiGenerator doesn't capture and share error information - would be nice to share error details when available
|
||||||
// or i'm capturing it incorrectly
|
// or i'm capturing it incorrectly
|
||||||
name: 'ModelListView',
|
name: "ModelListView",
|
||||||
mixins: [CardMixin, ApiMixin, ToastMixin],
|
mixins: [CardMixin, ApiMixin, ToastMixin],
|
||||||
components: {
|
components: {
|
||||||
GenericHorizontalCard, GenericModalForm, GenericInfiniteCards, ModelMenu,
|
GenericHorizontalCard,
|
||||||
},
|
GenericModalForm,
|
||||||
data() {
|
GenericInfiniteCards,
|
||||||
return {
|
ModelMenu,
|
||||||
// this.Models and this.Actions inherited from ApiMixin
|
|
||||||
items_left: [],
|
|
||||||
items_right: [],
|
|
||||||
right_counts: {'max': 9999, 'current': 0},
|
|
||||||
left_counts: {'max': 9999, 'current': 0},
|
|
||||||
this_model: undefined,
|
|
||||||
model_menu: undefined,
|
|
||||||
this_action: undefined,
|
|
||||||
this_recipe_param: undefined,
|
|
||||||
this_item: {},
|
|
||||||
this_target: {},
|
|
||||||
show_modal: false,
|
|
||||||
show_split: false,
|
|
||||||
paginated: false,
|
|
||||||
header_component_name: undefined,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
headerComponent() {
|
|
||||||
// TODO this leads webpack to create one .js file for each component in this folder because at runtime any one of them could be requested
|
|
||||||
// TODO this is not necessarily bad but maybe there are better options to do this
|
|
||||||
return () => import(/* webpackChunkName: "header-component" */ `@/components/${this.header_component_name}`)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
// value is passed from lists.py
|
|
||||||
let model_config = JSON.parse(document.getElementById('model_config').textContent)
|
|
||||||
this.this_model = this.Models[model_config?.model]
|
|
||||||
this.this_recipe_param = model_config?.recipe_param
|
|
||||||
this.paginated = this.this_model?.paginated ?? false
|
|
||||||
this.header_component_name = this.this_model?.list?.header_component?.name ?? undefined
|
|
||||||
this.$nextTick(() => {
|
|
||||||
if (!this.paginated) {
|
|
||||||
this.getItems({page:1},'left')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.$i18n.locale = window.CUSTOM_LOCALE
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
// this.genericAPI inherited from ApiMixin
|
|
||||||
resetList: function (e) {
|
|
||||||
this['items_' + e] = []
|
|
||||||
this[e + '_counts'].max = 9999 + Math.random()
|
|
||||||
this[e + '_counts'].current = 0
|
|
||||||
},
|
},
|
||||||
startAction: function (e, param) {
|
data() {
|
||||||
let source = e?.source ?? {}
|
return {
|
||||||
let target = e?.target ?? undefined
|
// this.Models and this.Actions inherited from ApiMixin
|
||||||
this.this_item = source
|
items_left: [],
|
||||||
this.this_target = target
|
items_right: [],
|
||||||
|
right_counts: { max: 9999, current: 0 },
|
||||||
switch (e.action) {
|
left_counts: { max: 9999, current: 0 },
|
||||||
case 'delete':
|
this_model: undefined,
|
||||||
this.this_action = this.Actions.DELETE
|
model_menu: undefined,
|
||||||
this.show_modal = true
|
this_action: undefined,
|
||||||
break;
|
this_recipe_param: undefined,
|
||||||
case 'new':
|
this_item: {},
|
||||||
this.this_action = this.Actions.CREATE
|
this_target: {},
|
||||||
this.show_modal = true
|
show_modal: false,
|
||||||
break;
|
show_split: false,
|
||||||
case 'edit':
|
paginated: false,
|
||||||
this.this_item = e.source
|
header_component_name: undefined,
|
||||||
this.this_action = this.Actions.UPDATE
|
|
||||||
this.show_modal = true
|
|
||||||
break;
|
|
||||||
case 'move':
|
|
||||||
if (target == null) {
|
|
||||||
this.this_item = e.source
|
|
||||||
this.this_action = this.Actions.MOVE
|
|
||||||
this.show_modal = true
|
|
||||||
} else {
|
|
||||||
this.moveThis(source.id, target.id)
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'merge':
|
|
||||||
if (target == null) {
|
|
||||||
this.this_item = e.source
|
|
||||||
this.this_action = this.Actions.MERGE
|
|
||||||
this.show_modal = true
|
|
||||||
} else {
|
|
||||||
this.mergeThis(e.source, e.target, false)
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'merge-automate':
|
|
||||||
if (target == null) {
|
|
||||||
this.this_item = e.source
|
|
||||||
this.this_action = this.Actions.MERGE
|
|
||||||
this.show_modal = true
|
|
||||||
} else {
|
|
||||||
this.mergeThis(e.source, e.target, true)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'get-children':
|
|
||||||
if (source.show_children) {
|
|
||||||
Vue.set(source, 'show_children', false)
|
|
||||||
} else {
|
|
||||||
this.getChildren(param, source)
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'get-recipes':
|
|
||||||
if (source.show_recipes) {
|
|
||||||
Vue.set(source, 'show_recipes', false)
|
|
||||||
} else {
|
|
||||||
this.getRecipes(param, source)
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
finishAction: function (e) {
|
|
||||||
let update = undefined
|
|
||||||
switch (e?.action) {
|
|
||||||
case 'save':
|
|
||||||
this.saveThis(e.form_data)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (e !== 'cancel') {
|
|
||||||
switch (this.this_action) {
|
|
||||||
case this.Actions.DELETE:
|
|
||||||
this.deleteThis(this.this_item.id)
|
|
||||||
break;
|
|
||||||
case this.Actions.CREATE:
|
|
||||||
this.saveThis(e.form_data)
|
|
||||||
break;
|
|
||||||
case this.Actions.UPDATE:
|
|
||||||
update = e.form_data
|
|
||||||
update.id = this.this_item.id
|
|
||||||
this.saveThis(update)
|
|
||||||
break;
|
|
||||||
case this.Actions.MERGE:
|
|
||||||
this.mergeThis(this.this_item, e.form_data.target, false)
|
|
||||||
break;
|
|
||||||
case this.Actions.MOVE:
|
|
||||||
this.moveThis(this.this_item.id, e.form_data.target.id)
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
this.clearState()
|
|
||||||
},
|
},
|
||||||
getItems: function (params, col) {
|
computed: {
|
||||||
let column = col || 'left'
|
headerComponent() {
|
||||||
params.options = {'query':{'extended': 1}} // returns extended values in API response
|
// TODO this leads webpack to create one .js file for each component in this folder because at runtime any one of them could be requested
|
||||||
this.genericAPI(this.this_model, this.Actions.LIST, params).then((result) => {
|
// TODO this is not necessarily bad but maybe there are better options to do this
|
||||||
let results = result.data?.results ?? result.data
|
return () => import(/* webpackChunkName: "header-component" */ `@/components/${this.header_component_name}`)
|
||||||
|
},
|
||||||
if (results?.length) {
|
|
||||||
|
|
||||||
// let secondaryRequest = undefined;
|
|
||||||
// if (this['items_' + column]?.length < getConfig(this.this_model, this.Actions.LIST).config.pageSize.default * (params.page - 1)) {
|
|
||||||
// // the item list is smaller than it should be based on the site the user is own
|
|
||||||
// // this happens when an item is deleted (or merged)
|
|
||||||
// // to prevent issues insert the last item of the previous search page before loading the new results
|
|
||||||
// params.page = params.page - 1
|
|
||||||
// secondaryRequest = this.genericAPI(this.this_model, this.Actions.LIST, params).then((result) => {
|
|
||||||
// let prev_page_results = result.data?.results ?? result.data
|
|
||||||
// if (prev_page_results?.length) {
|
|
||||||
// results = [prev_page_results[prev_page_results.length]].concat(results)
|
|
||||||
//
|
|
||||||
// this['items_' + column] = this['items_' + column].concat(results) //TODO duplicate code, find some elegant workaround
|
|
||||||
// this[column + '_counts']['current'] = getConfig(this.this_model, this.Actions.LIST).config.pageSize.default * (params.page - 1) + results.length
|
|
||||||
// this[column + '_counts']['max'] = result.data?.count ?? 0
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// } else {
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
|
|
||||||
this['items_' + column] = this['items_' + column].concat(results)
|
|
||||||
this[column + '_counts']['current'] = getConfig(this.this_model, this.Actions.LIST).config.pageSize.default * (params.page - 1) + results.length
|
|
||||||
this[column + '_counts']['max'] = result.data?.count ?? 0
|
|
||||||
|
|
||||||
} else {
|
|
||||||
this[column + '_counts']['max'] = 0
|
|
||||||
this[column + '_counts']['current'] = 0
|
|
||||||
console.log('no data returned')
|
|
||||||
}
|
|
||||||
}).catch((err) => {
|
|
||||||
console.log(err, Object.keys(err))
|
|
||||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
getThis: function (id, callback) {
|
mounted() {
|
||||||
return this.genericAPI(this.this_model, this.Actions.FETCH, {'id': id})
|
// value is passed from lists.py
|
||||||
},
|
let model_config = JSON.parse(document.getElementById("model_config").textContent)
|
||||||
saveThis: function (thisItem) {
|
this.this_model = this.Models[model_config?.model]
|
||||||
if (!thisItem?.id) { // if there is no item id assume it's a new item
|
this.this_recipe_param = model_config?.recipe_param
|
||||||
this.genericAPI(this.this_model, this.Actions.CREATE, thisItem).then((result) => {
|
this.paginated = this.this_model?.paginated ?? false
|
||||||
// look for and destroy any existing cards to prevent duplicates in the GET case of get_or_create
|
this.header_component_name = this.this_model?.list?.header_component?.name ?? undefined
|
||||||
// then place all new items at the top of the list - could sort instead
|
this.$nextTick(() => {
|
||||||
this.items_left = [result.data].concat(this.destroyCard(result?.data?.id, this.items_left))
|
if (!this.paginated) {
|
||||||
// this creates a deep copy to make sure that columns stay independent
|
this.getItems({ page: 1 }, "left")
|
||||||
this.items_right = [{...result.data}].concat(this.destroyCard(result?.data?.id, this.items_right))
|
}
|
||||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
|
||||||
}).catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
|
|
||||||
})
|
})
|
||||||
} else {
|
this.$i18n.locale = window.CUSTOM_LOCALE
|
||||||
this.genericAPI(this.this_model, this.Actions.UPDATE, thisItem).then((result) => {
|
|
||||||
this.refreshThis(thisItem.id)
|
|
||||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE)
|
|
||||||
}).catch((err) => {
|
|
||||||
console.log(err, err.response)
|
|
||||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
moveThis: function (source_id, target_id) {
|
methods: {
|
||||||
if (source_id === target_id) {
|
// this.genericAPI inherited from ApiMixin
|
||||||
this.makeToast(this.$t('Error'), this.$t('Cannot move item to itself'), 'danger')
|
resetList: function (e) {
|
||||||
this.clearState()
|
this["items_" + e] = []
|
||||||
return
|
this[e + "_counts"].max = 9999 + Math.random()
|
||||||
}
|
this[e + "_counts"].current = 0
|
||||||
let item = this.findCard(source_id, this.items_left) || this.findCard(source_id, this.items_right)
|
},
|
||||||
if (source_id === undefined || target_id === undefined || item?.parent == target_id) {
|
startAction: function (e, param) {
|
||||||
this.makeToast(this.$t('Warning'), this.$t('Nothing to do'), 'warning')
|
let source = e?.source ?? {}
|
||||||
this.clearState()
|
let target = e?.target ?? undefined
|
||||||
return
|
this.this_item = source
|
||||||
}
|
this.this_target = target
|
||||||
this.genericAPI(this.this_model, this.Actions.MOVE, {'source': source_id, 'target': target_id}).then((result) => {
|
|
||||||
if (target_id === 0) {
|
|
||||||
this.items_left = [item].concat(this.destroyCard(source_id, this.items_left)) // order matters, destroy old card before adding it back in at root
|
|
||||||
this.items_right = [...[item]].concat(this.destroyCard(source_id, this.items_right)) // order matters, destroy old card before adding it back in at root
|
|
||||||
item.parent = null
|
|
||||||
} else {
|
|
||||||
this.items_left = this.destroyCard(source_id, this.items_left)
|
|
||||||
this.items_right = this.destroyCard(source_id, this.items_right)
|
|
||||||
this.refreshThis(target_id)
|
|
||||||
}
|
|
||||||
// TODO make standard toast
|
|
||||||
this.makeToast(this.$t('Success'), 'Succesfully moved resource', 'success')
|
|
||||||
}).catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
mergeThis: function (source, target, automate) {
|
|
||||||
let source_id = source.id
|
|
||||||
let target_id = target.id
|
|
||||||
if (source_id === target_id) {
|
|
||||||
this.makeToast(this.$t('Error'), this.$t('Cannot merge item with itself'), 'danger')
|
|
||||||
this.clearState()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!source_id || !target_id) {
|
|
||||||
this.makeToast(this.$t('Warning'), this.$t('Nothing to do'), 'warning')
|
|
||||||
this.clearState()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.genericAPI(this.this_model, this.Actions.MERGE, {
|
|
||||||
'source': source_id,
|
|
||||||
'target': target_id
|
|
||||||
}).then((result) => {
|
|
||||||
this.items_left = this.destroyCard(source_id, this.items_left)
|
|
||||||
this.items_right = this.destroyCard(source_id, this.items_right)
|
|
||||||
this.refreshThis(target_id)
|
|
||||||
// TODO make standard toast
|
|
||||||
this.makeToast(this.$t('Success'), 'Succesfully merged resource', 'success')
|
|
||||||
}).catch((err) => {
|
|
||||||
//TODO error checking not working with OpenAPI methods
|
|
||||||
console.log('Error', err)
|
|
||||||
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
|
|
||||||
})
|
|
||||||
|
|
||||||
if (automate) {
|
switch (e.action) {
|
||||||
let apiClient = new ApiApiFactory()
|
case "delete":
|
||||||
|
this.this_action = this.Actions.DELETE
|
||||||
|
this.show_modal = true
|
||||||
|
break
|
||||||
|
case "new":
|
||||||
|
this.this_action = this.Actions.CREATE
|
||||||
|
this.show_modal = true
|
||||||
|
break
|
||||||
|
case "edit":
|
||||||
|
this.this_item = e.source
|
||||||
|
this.this_action = this.Actions.UPDATE
|
||||||
|
this.show_modal = true
|
||||||
|
break
|
||||||
|
case "move":
|
||||||
|
if (target == null) {
|
||||||
|
this.this_item = e.source
|
||||||
|
this.this_action = this.Actions.MOVE
|
||||||
|
this.show_modal = true
|
||||||
|
} else {
|
||||||
|
// this is redundant - function also exists in GenericModal
|
||||||
|
this.moveThis(source.id, target.id)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "merge":
|
||||||
|
if (target == null) {
|
||||||
|
this.this_item = e.source
|
||||||
|
this.this_action = this.Actions.MERGE
|
||||||
|
this.show_modal = true
|
||||||
|
} else {
|
||||||
|
// this is redundant - function also exists in GenericModal
|
||||||
|
this.mergeThis(e.source, e.target, false)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "merge-automate":
|
||||||
|
if (target == null) {
|
||||||
|
this.this_item = e.source
|
||||||
|
this.this_action = this.Actions.MERGE
|
||||||
|
this.this_item.automate = true
|
||||||
|
this.show_modal = true
|
||||||
|
} else {
|
||||||
|
// this is redundant - function also exists in GenericModal
|
||||||
|
this.mergeThis(e.source, e.target, true)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "get-children":
|
||||||
|
if (source.show_children) {
|
||||||
|
Vue.set(source, "show_children", false)
|
||||||
|
} else {
|
||||||
|
this.getChildren(param, source)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "get-recipes":
|
||||||
|
if (source.show_recipes) {
|
||||||
|
Vue.set(source, "show_recipes", false)
|
||||||
|
} else {
|
||||||
|
this.getRecipes(param, source)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
finishAction: function (e) {
|
||||||
|
switch (e?.action) {
|
||||||
|
case "save":
|
||||||
|
this.saveThis(e.form_data)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (e !== "cancel") {
|
||||||
|
switch (this.this_action) {
|
||||||
|
case this.Actions.DELETE:
|
||||||
|
console.log("delete")
|
||||||
|
this.deleteThis(this.this_item.id)
|
||||||
|
break
|
||||||
|
case this.Actions.CREATE:
|
||||||
|
this.saveThis(e.item)
|
||||||
|
break
|
||||||
|
case this.Actions.UPDATE:
|
||||||
|
this.updateThis(this.this_item)
|
||||||
|
break
|
||||||
|
case this.Actions.MERGE:
|
||||||
|
this.mergeUpdateItem(this.this_item.id, e.target)
|
||||||
|
break
|
||||||
|
case this.Actions.MOVE:
|
||||||
|
this.moveUpdateItem(this.this_item.id, e.target)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.clearState()
|
||||||
|
},
|
||||||
|
getItems: function (params, col) {
|
||||||
|
let column = col || "left"
|
||||||
|
params.options = { query: { extended: 1 } } // returns extended values in API response
|
||||||
|
this.genericAPI(this.this_model, this.Actions.LIST, params)
|
||||||
|
.then((result) => {
|
||||||
|
let results = result.data?.results ?? result.data
|
||||||
|
|
||||||
let automation = {
|
if (results?.length) {
|
||||||
name: `Merge ${source.name} with ${target.name}`,
|
// let secondaryRequest = undefined;
|
||||||
param_1: source.name,
|
// if (this['items_' + column]?.length < getConfig(this.this_model, this.Actions.LIST).config.pageSize.default * (params.page - 1)) {
|
||||||
param_2: target.name
|
// // the item list is smaller than it should be based on the site the user is own
|
||||||
}
|
// // this happens when an item is deleted (or merged)
|
||||||
|
// // to prevent issues insert the last item of the previous search page before loading the new results
|
||||||
|
// params.page = params.page - 1
|
||||||
|
// secondaryRequest = this.genericAPI(this.this_model, this.Actions.LIST, params).then((result) => {
|
||||||
|
// let prev_page_results = result.data?.results ?? result.data
|
||||||
|
// if (prev_page_results?.length) {
|
||||||
|
// results = [prev_page_results[prev_page_results.length]].concat(results)
|
||||||
|
//
|
||||||
|
// this['items_' + column] = this['items_' + column].concat(results) //TODO duplicate code, find some elegant workaround
|
||||||
|
// this[column + '_counts']['current'] = getConfig(this.this_model, this.Actions.LIST).config.pageSize.default * (params.page - 1) + results.length
|
||||||
|
// this[column + '_counts']['max'] = result.data?.count ?? 0
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// } else {
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
|
||||||
if (this.this_model === this.Models.FOOD) {
|
this["items_" + column] = this["items_" + column].concat(results)
|
||||||
automation.type = 'FOOD_ALIAS'
|
this[column + "_counts"]["current"] = getConfig(this.this_model, this.Actions.LIST).config.pageSize.default * (params.page - 1) + results.length
|
||||||
}
|
this[column + "_counts"]["max"] = result.data?.count ?? 0
|
||||||
if (this.this_model === this.Models.UNIT) {
|
} else {
|
||||||
automation.type = 'UNIT_ALIAS'
|
this[column + "_counts"]["max"] = 0
|
||||||
}
|
this[column + "_counts"]["current"] = 0
|
||||||
if (this.this_model === this.Models.KEYWORD) {
|
console.log("no data returned")
|
||||||
automation.type = 'KEYWORD_ALIAS'
|
}
|
||||||
}
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err, Object.keys(err))
|
||||||
|
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getThis: function (id, callback) {
|
||||||
|
return this.genericAPI(this.this_model, this.Actions.FETCH, { id: id })
|
||||||
|
},
|
||||||
|
saveThis: function (item) {
|
||||||
|
// look for and destroy any existing cards to prevent duplicates in the GET case of get_or_create
|
||||||
|
// then place all new items at the top of the list - could sort instead
|
||||||
|
this.items_left = [item].concat(this.destroyCard(item?.id, this.items_left))
|
||||||
|
// this creates a deep copy to make sure that columns stay independent
|
||||||
|
this.items_right = [{ ...item }].concat(this.destroyCard(item?.id, this.items_right))
|
||||||
|
},
|
||||||
|
updateThis: function (item) {
|
||||||
|
this.refreshThis(item.id)
|
||||||
|
},
|
||||||
|
moveThis: function (source_id, target_id) {
|
||||||
|
// TODO: this function is almost 100% duplicated in GenericModalForm and only exists to enable drag and drop
|
||||||
|
if (source_id === target_id) {
|
||||||
|
this.makeToast(this.$t("Error"), this.$t("err_move_self"), "danger")
|
||||||
|
this.clearState()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let item = this.findCard(source_id, this.items_left) || this.findCard(source_id, this.items_right)
|
||||||
|
if (source_id === undefined || target_id === undefined || item?.parent == target_id) {
|
||||||
|
this.makeToast(this.$t("Warning"), this.$t("nothing"), "warning")
|
||||||
|
this.clearState()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.genericAPI(this.this_model, this.Actions.MOVE, { source: source_id, target: target_id })
|
||||||
|
.then((result) => {
|
||||||
|
this.moveUpdateItem(source_id, target_id)
|
||||||
|
// TODO make standard toast
|
||||||
|
this.makeToast(this.$t("Success"), "Succesfully moved resource", "success")
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
this.makeToast(this.$t("Error"), err.bodyText, "danger")
|
||||||
|
})
|
||||||
|
},
|
||||||
|
moveUpdateItem: function (source_id, target_id) {
|
||||||
|
let item = this.findCard(source_id, this.items_left) || this.findCard(source_id, this.items_right)
|
||||||
|
if (target_id === 0) {
|
||||||
|
this.items_left = [item].concat(this.destroyCard(source_id, this.items_left)) // order matters, destroy old card before adding it back in at root
|
||||||
|
this.items_right = [...[item]].concat(this.destroyCard(source_id, this.items_right)) // order matters, destroy old card before adding it back in at root
|
||||||
|
item.parent = null
|
||||||
|
} else {
|
||||||
|
this.items_left = this.destroyCard(source_id, this.items_left)
|
||||||
|
this.items_right = this.destroyCard(source_id, this.items_right)
|
||||||
|
this.refreshThis(target_id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mergeThis: function (source, target, automate) {
|
||||||
|
// TODO: this function is almost 100% duplicated in GenericModalForm and only exists to enable drag and drop
|
||||||
|
let source_id = source.id
|
||||||
|
let target_id = target.id
|
||||||
|
if (source_id === target_id) {
|
||||||
|
this.makeToast(this.$t("Error"), this.$t("err_merge_self"), "danger")
|
||||||
|
this.clearState()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!source_id || !target_id) {
|
||||||
|
this.makeToast(this.$t("Warning"), this.$t("nothing"), "warning")
|
||||||
|
this.clearState()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.genericAPI(this.this_model, this.Actions.MERGE, {
|
||||||
|
source: source_id,
|
||||||
|
target: target_id,
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
this.mergeUpdateItem(source_id, target_id)
|
||||||
|
// TODO make standard toast
|
||||||
|
this.makeToast(this.$t("Success"), "Succesfully merged resource", "success")
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
//TODO error checking not working with OpenAPI methods
|
||||||
|
console.log("Error", err)
|
||||||
|
this.makeToast(this.$t("Error"), err.bodyText, "danger")
|
||||||
|
})
|
||||||
|
|
||||||
apiClient.createAutomation(automation)
|
if (automate) {
|
||||||
}
|
let apiClient = new ApiApiFactory()
|
||||||
|
|
||||||
},
|
let automation = {
|
||||||
getChildren: function (col, item) {
|
name: `Merge ${source.name} with ${target.name}`,
|
||||||
let parent = {}
|
param_1: source.name,
|
||||||
let params = {
|
param_2: target.name,
|
||||||
'root': item.id,
|
}
|
||||||
'pageSize': 200,
|
|
||||||
'query': {'extended': 1},
|
|
||||||
'options': {'query':{'extended': 1}}
|
|
||||||
}
|
|
||||||
this.genericAPI(this.this_model, this.Actions.LIST, params).then((result) => {
|
|
||||||
parent = this.findCard(item.id, this['items_' + col])
|
|
||||||
if (parent) {
|
|
||||||
Vue.set(parent, 'children', result.data.results)
|
|
||||||
Vue.set(parent, 'show_children', true)
|
|
||||||
Vue.set(parent, 'show_recipes', false)
|
|
||||||
}
|
|
||||||
}).catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getRecipes: function (col, item) {
|
|
||||||
let parent = {}
|
|
||||||
// TODO: make this generic
|
|
||||||
let params = {'pageSize': 50}
|
|
||||||
params[this.this_recipe_param] = item.id
|
|
||||||
console.log('RECIPE PARAM', this.this_recipe_param, params, item.id)
|
|
||||||
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params).then((result) => {
|
|
||||||
parent = this.findCard(item.id, this['items_' + col])
|
|
||||||
if (parent) {
|
|
||||||
Vue.set(parent, 'recipes', result.data.results)
|
|
||||||
Vue.set(parent, 'show_recipes', true)
|
|
||||||
Vue.set(parent, 'show_children', false)
|
|
||||||
}
|
|
||||||
|
|
||||||
}).catch((err) => {
|
if (this.this_model === this.Models.FOOD) {
|
||||||
console.log(err)
|
automation.type = "FOOD_ALIAS"
|
||||||
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
|
}
|
||||||
})
|
if (this.this_model === this.Models.UNIT) {
|
||||||
|
automation.type = "UNIT_ALIAS"
|
||||||
|
}
|
||||||
|
if (this.this_model === this.Models.KEYWORD) {
|
||||||
|
automation.type = "KEYWORD_ALIAS"
|
||||||
|
}
|
||||||
|
|
||||||
|
apiClient.createAutomation(automation)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mergeUpdateItem: function (source, target, automate) {
|
||||||
|
this.items_left = this.destroyCard(source, this.items_left)
|
||||||
|
this.items_right = this.destroyCard(source, this.items_right)
|
||||||
|
this.refreshThis(target)
|
||||||
|
},
|
||||||
|
getChildren: function (col, item) {
|
||||||
|
let parent = {}
|
||||||
|
let params = {
|
||||||
|
root: item.id,
|
||||||
|
pageSize: 200,
|
||||||
|
query: { extended: 1 },
|
||||||
|
options: { query: { extended: 1 } },
|
||||||
|
}
|
||||||
|
this.genericAPI(this.this_model, this.Actions.LIST, params)
|
||||||
|
.then((result) => {
|
||||||
|
parent = this.findCard(item.id, this["items_" + col])
|
||||||
|
if (parent) {
|
||||||
|
Vue.set(parent, "children", result.data.results)
|
||||||
|
Vue.set(parent, "show_children", true)
|
||||||
|
Vue.set(parent, "show_recipes", false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
this.makeToast(this.$t("Error"), err.bodyText, "danger")
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getRecipes: function (col, item) {
|
||||||
|
let parent = {}
|
||||||
|
// TODO: make this generic
|
||||||
|
let params = { pageSize: 50 }
|
||||||
|
params[this.this_recipe_param] = item.id
|
||||||
|
console.log("RECIPE PARAM", this.this_recipe_param, params, item.id)
|
||||||
|
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params)
|
||||||
|
.then((result) => {
|
||||||
|
parent = this.findCard(item.id, this["items_" + col])
|
||||||
|
if (parent) {
|
||||||
|
Vue.set(parent, "recipes", result.data.results)
|
||||||
|
Vue.set(parent, "show_recipes", true)
|
||||||
|
Vue.set(parent, "show_children", false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
this.makeToast(this.$t("Error"), err.bodyText, "danger")
|
||||||
|
})
|
||||||
|
},
|
||||||
|
refreshThis: function (id) {
|
||||||
|
this.getThis(id).then((result) => {
|
||||||
|
this.refreshCard(result.data, this.items_left)
|
||||||
|
this.refreshCard({ ...result.data }, this.items_right)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deleteThis: function (id) {
|
||||||
|
this.items_left = this.destroyCard(id, this.items_left)
|
||||||
|
this.items_right = this.destroyCard(id, this.items_right)
|
||||||
|
},
|
||||||
|
clearState: function () {
|
||||||
|
this.show_modal = false
|
||||||
|
this.this_action = undefined
|
||||||
|
this.this_item = undefined
|
||||||
|
this.this_target = undefined
|
||||||
|
},
|
||||||
},
|
},
|
||||||
refreshThis: function (id) {
|
|
||||||
this.getThis(id).then(result => {
|
|
||||||
this.refreshCard(result.data, this.items_left)
|
|
||||||
this.refreshCard({...result.data}, this.items_right)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
deleteThis: function (id) {
|
|
||||||
this.genericAPI(this.this_model, this.Actions.DELETE, {'id': id}).then((result) => {
|
|
||||||
this.items_left = this.destroyCard(id, this.items_left)
|
|
||||||
this.items_right = this.destroyCard(id, this.items_right)
|
|
||||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_DELETE)
|
|
||||||
}).catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_DELETE)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
clearState: function () {
|
|
||||||
this.show_modal = false
|
|
||||||
this.this_action = undefined
|
|
||||||
this.this_item = undefined
|
|
||||||
this.this_target = undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
|
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
|
||||||
|
|
||||||
<style>
|
<style></style>
|
||||||
|
|
||||||
</style>
|
|
||||||
|
@ -1,44 +1,45 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="itemList">
|
<div v-if="itemList">
|
||||||
<span :key="k.id" v-for="k in itemList" class="pl-1">
|
<span :key="k.id" v-for="k in itemList" class="pl-1">
|
||||||
<b-badge pill :variant="color">{{thisLabel(k)}}</b-badge>
|
<b-badge pill :variant="color">{{ thisLabel(k) }}</b-badge>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'GenericPill',
|
name: "GenericPill",
|
||||||
props: {
|
props: {
|
||||||
item_list: {required: true, type: Array},
|
item_list: {
|
||||||
label: {type: String, default: 'name'},
|
type: Array,
|
||||||
color: {type: String, default: 'light'}
|
default() {
|
||||||
},
|
return []
|
||||||
computed: {
|
},
|
||||||
itemList: function() {
|
},
|
||||||
if(Array.isArray(this.item_list)) {
|
label: { type: String, default: "name" },
|
||||||
return this.item_list
|
color: { type: String, default: "light" },
|
||||||
} else if (!this.item_list?.id) {
|
},
|
||||||
return false
|
computed: {
|
||||||
} else {
|
itemList: function () {
|
||||||
return [this.item_list]
|
if (Array.isArray(this.item_list)) {
|
||||||
}
|
return this.item_list
|
||||||
|
} else if (!this.item_list?.id) {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return [this.item_list]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {},
|
||||||
|
methods: {
|
||||||
|
thisLabel: function (item) {
|
||||||
|
let fields = this.label.split("::")
|
||||||
|
let value = item
|
||||||
|
fields.forEach((x) => {
|
||||||
|
value = value[x]
|
||||||
|
})
|
||||||
|
return value
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
thisLabel: function (item) {
|
|
||||||
let fields = this.label.split('::')
|
|
||||||
let value = item
|
|
||||||
fields.forEach(x => {
|
|
||||||
value = value[x]
|
|
||||||
});
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,143 +1,250 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<b-modal :id="'modal_'+id" @hidden="cancelAction">
|
<b-modal :id="'modal_' + id" @hidden="cancelAction">
|
||||||
<template v-slot:modal-title><h4>{{ form.title }}</h4></template>
|
<template v-slot:modal-title
|
||||||
<div v-for="(f, i) in form.fields" v-bind:key=i>
|
><h4>{{ form.title }}</h4></template
|
||||||
<p v-if="f.type=='instruction'">{{ f.label }}</p>
|
>
|
||||||
<!-- this lookup is single selection -->
|
<div v-for="(f, i) in form.fields" v-bind:key="i">
|
||||||
<lookup-input v-if="f.type=='lookup'"
|
<p v-if="f.type == 'instruction'">{{ f.label }}</p>
|
||||||
:form="f"
|
<!-- this lookup is single selection -->
|
||||||
:model="listModel(f.list)"
|
<lookup-input v-if="f.type == 'lookup'" :form="f" :model="listModel(f.list)" @change="storeValue" />
|
||||||
@change="storeValue"/> <!-- TODO add ability to create new items associated with lookup -->
|
<!-- TODO add ability to create new items associated with lookup -->
|
||||||
<!-- TODO: add multi-selection input list -->
|
<!-- TODO: add multi-selection input list -->
|
||||||
<checkbox-input v-if="f.type=='checkbox'"
|
<checkbox-input v-if="f.type == 'checkbox'" :label="f.label" :value="f.value" :field="f.field" />
|
||||||
:label="f.label"
|
<text-input v-if="f.type == 'text'" :label="f.label" :value="f.value" :field="f.field" :placeholder="f.placeholder" />
|
||||||
:value="f.value"
|
<choice-input v-if="f.type == 'choice'" :label="f.label" :value="f.value" :field="f.field" :options="f.options" :placeholder="f.placeholder" />
|
||||||
:field="f.field"/>
|
<emoji-input v-if="f.type == 'emoji'" :label="f.label" :value="f.value" :field="f.field" @change="storeValue" />
|
||||||
<text-input v-if="f.type=='text'"
|
<file-input v-if="f.type == 'file'" :label="f.label" :value="f.value" :field="f.field" @change="storeValue" />
|
||||||
:label="f.label"
|
</div>
|
||||||
:value="f.value"
|
|
||||||
:field="f.field"
|
|
||||||
:placeholder="f.placeholder"/>
|
|
||||||
<choice-input v-if="f.type=='choice'"
|
|
||||||
:label="f.label"
|
|
||||||
:value="f.value"
|
|
||||||
:field="f.field"
|
|
||||||
:options="f.options"
|
|
||||||
:placeholder="f.placeholder"/>
|
|
||||||
<emoji-input v-if="f.type=='emoji'"
|
|
||||||
:label="f.label"
|
|
||||||
:value="f.value"
|
|
||||||
:field="f.field"
|
|
||||||
@change="storeValue"/>
|
|
||||||
<file-input v-if="f.type=='file'"
|
|
||||||
:label="f.label"
|
|
||||||
:value="f.value"
|
|
||||||
:field="f.field"
|
|
||||||
@change="storeValue"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template v-slot:modal-footer>
|
<template v-slot:modal-footer>
|
||||||
<b-button class="float-right mx-1" variant="secondary" v-on:click="cancelAction">{{ $t('Cancel') }}</b-button>
|
<b-button class="float-right mx-1" variant="secondary" v-on:click="cancelAction">{{ $t("Cancel") }}</b-button>
|
||||||
<b-button class="float-right mx-1" variant="primary" v-on:click="doAction">{{ form.ok_label }}</b-button>
|
<b-button class="float-right mx-1" variant="primary" v-on:click="doAction">{{ form.ok_label }}</b-button>
|
||||||
</template>
|
</template>
|
||||||
</b-modal>
|
</b-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Vue from 'vue'
|
import Vue from "vue"
|
||||||
import {BootstrapVue} from 'bootstrap-vue'
|
import { BootstrapVue } from "bootstrap-vue"
|
||||||
import {getForm} from "@/utils/utils";
|
import { getForm } from "@/utils/utils"
|
||||||
|
|
||||||
Vue.use(BootstrapVue)
|
Vue.use(BootstrapVue)
|
||||||
|
|
||||||
import {Models} from "@/utils/models";
|
import { ApiApiFactory } from "@/utils/openapi/api"
|
||||||
import CheckboxInput from "@/components/Modals/CheckboxInput";
|
import { ApiMixin, StandardToasts, ToastMixin } from "@/utils/utils"
|
||||||
import LookupInput from "@/components/Modals/LookupInput";
|
import CheckboxInput from "@/components/Modals/CheckboxInput"
|
||||||
import TextInput from "@/components/Modals/TextInput";
|
import LookupInput from "@/components/Modals/LookupInput"
|
||||||
import EmojiInput from "@/components/Modals/EmojiInput";
|
import TextInput from "@/components/Modals/TextInput"
|
||||||
import ChoiceInput from "@/components/Modals/ChoiceInput";
|
import EmojiInput from "@/components/Modals/EmojiInput"
|
||||||
import FileInput from "@/components/Modals/FileInput";
|
import ChoiceInput from "@/components/Modals/ChoiceInput"
|
||||||
|
import FileInput from "@/components/Modals/FileInput"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'GenericModalForm',
|
name: "GenericModalForm",
|
||||||
components: {FileInput, CheckboxInput, LookupInput, TextInput, EmojiInput, ChoiceInput},
|
components: { FileInput, CheckboxInput, LookupInput, TextInput, EmojiInput, ChoiceInput },
|
||||||
props: {
|
mixins: [ApiMixin, ToastMixin],
|
||||||
model: {required: true, type: Object},
|
props: {
|
||||||
action: {required: true, type: Object},
|
model: { required: true, type: Object },
|
||||||
item1: {
|
action: { type: Object },
|
||||||
type: Object, default() {
|
item1: {
|
||||||
return undefined
|
type: Object,
|
||||||
}
|
default() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
item2: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
show: { required: true, type: Boolean, default: false },
|
||||||
},
|
},
|
||||||
item2: {
|
data() {
|
||||||
type: Object, default() {
|
return {
|
||||||
return undefined
|
id: undefined,
|
||||||
}
|
form_data: {},
|
||||||
},
|
form: {},
|
||||||
show: {required: true, type: Boolean, default: false},
|
dirty: false,
|
||||||
},
|
special_handling: false,
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
id: undefined,
|
|
||||||
form_data: {},
|
|
||||||
form: {},
|
|
||||||
dirty: false,
|
|
||||||
special_handling: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.id = Math.random()
|
|
||||||
this.$root.$on('change', this.storeValue); // boostrap modal placed at document so have to listen at root of component
|
|
||||||
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
buttonLabel() {
|
|
||||||
return this.buttons[this.action].label;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
'show': function () {
|
|
||||||
if (this.show) {
|
|
||||||
this.form = getForm(this.model, this.action, this.item1, this.item2)
|
|
||||||
this.dirty = true
|
|
||||||
this.$bvModal.show('modal_' + this.id)
|
|
||||||
} else {
|
|
||||||
this.$bvModal.hide('modal_' + this.id)
|
|
||||||
this.form_data = {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
doAction: function () {
|
|
||||||
this.dirty = false
|
|
||||||
this.$emit('finish-action', {'form_data': this.detectOverride(this.form_data)})
|
|
||||||
},
|
|
||||||
cancelAction: function () {
|
|
||||||
if (this.dirty) {
|
|
||||||
this.dirty = false
|
|
||||||
this.$emit('finish-action', 'cancel')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
storeValue: function (field, value) {
|
|
||||||
this.form_data[field] = value
|
|
||||||
},
|
|
||||||
listModel: function (m) {
|
|
||||||
if (m === 'self') {
|
|
||||||
return this.model
|
|
||||||
} else {
|
|
||||||
return Models[m]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
detectOverride: function (form) {
|
|
||||||
for (const [k, v] of Object.entries(form)) {
|
|
||||||
if (form[k].__override__) {
|
|
||||||
form[k] = form[k].__override__
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
return form
|
mounted() {
|
||||||
}
|
this.id = Math.random()
|
||||||
}
|
this.$root.$on("change", this.storeValue) // boostrap modal placed at document so have to listen at root of component
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
buttonLabel() {
|
||||||
|
return this.buttons[this.action].label
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show: function () {
|
||||||
|
if (this.show) {
|
||||||
|
this.form = getForm(this.model, this.action, this.item1, this.item2)
|
||||||
|
this.dirty = true
|
||||||
|
this.$bvModal.show("modal_" + this.id)
|
||||||
|
} else {
|
||||||
|
this.$bvModal.hide("modal_" + this.id)
|
||||||
|
this.form_data = {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
doAction: function () {
|
||||||
|
this.dirty = false
|
||||||
|
switch (this.action) {
|
||||||
|
case this.Actions.DELETE:
|
||||||
|
this.delete()
|
||||||
|
break
|
||||||
|
case this.Actions.CREATE:
|
||||||
|
this.save()
|
||||||
|
break
|
||||||
|
case this.Actions.UPDATE:
|
||||||
|
this.form_data.id = this.item1.id
|
||||||
|
this.save()
|
||||||
|
break
|
||||||
|
case this.Actions.MERGE:
|
||||||
|
this.merge(this.item1, this.form_data.target.id, this.item1?.automate ?? false)
|
||||||
|
break
|
||||||
|
case this.Actions.MOVE:
|
||||||
|
this.move(this.item1.id, this.form_data.target.id)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cancelAction: function () {
|
||||||
|
if (this.dirty) {
|
||||||
|
this.dirty = false
|
||||||
|
this.$emit("finish-action", "cancel")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
storeValue: function (field, value) {
|
||||||
|
this.form_data[field] = value
|
||||||
|
},
|
||||||
|
listModel: function (m) {
|
||||||
|
if (m === "self") {
|
||||||
|
return this.model
|
||||||
|
} else {
|
||||||
|
return this.Models[m]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
detectOverride: function (form) {
|
||||||
|
for (const [k, v] of Object.entries(form)) {
|
||||||
|
if (form[k].__override__) {
|
||||||
|
form[k] = form[k].__override__
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return form
|
||||||
|
},
|
||||||
|
delete: function () {
|
||||||
|
this.genericAPI(this.model, this.Actions.DELETE, { id: this.item1.id })
|
||||||
|
.then((result) => {
|
||||||
|
this.$emit("finish-action")
|
||||||
|
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_DELETE)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
StandardToasts.makeStandardToast(StandardToasts.FAIL_DELETE)
|
||||||
|
this.$emit("finish-action", "cancel")
|
||||||
|
})
|
||||||
|
},
|
||||||
|
save: function () {
|
||||||
|
if (!this.item1?.id) {
|
||||||
|
// if there is no item id assume it's a new item
|
||||||
|
this.genericAPI(this.model, this.Actions.CREATE, this.form_data)
|
||||||
|
.then((result) => {
|
||||||
|
this.$emit("finish-action", { item: result.data })
|
||||||
|
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
|
||||||
|
this.$emit("finish-action", "cancel")
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.genericAPI(this.model, this.Actions.UPDATE, this.form_data)
|
||||||
|
.then((result) => {
|
||||||
|
this.$emit("finish-action")
|
||||||
|
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err, err.response)
|
||||||
|
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
|
||||||
|
this.$emit("finish-action", "cancel")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
move: function () {
|
||||||
|
if (this.item1.id === this.form_data.target.id) {
|
||||||
|
this.makeToast(this.$t("Error"), this.$t("err_move_self"), "danger")
|
||||||
|
this.$emit("finish-action", "cancel")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.form_data.target.id === undefined || this.item1?.parent == this.form_data.target.id) {
|
||||||
|
this.makeToast(this.$t("Warning"), this.$t("nothing"), "warning")
|
||||||
|
this.$emit("finish-action", "cancel")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.genericAPI(this.model, this.Actions.MOVE, { source: this.item1.id, target: this.form_data.target.id })
|
||||||
|
.then((result) => {
|
||||||
|
this.$emit("finish-action", { target: this.form_data.target.id })
|
||||||
|
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_MOVE)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
StandardToasts.makeStandardToast(StandardToasts.FAIL_MOVE)
|
||||||
|
this.$emit("finish-action", "cancel")
|
||||||
|
})
|
||||||
|
},
|
||||||
|
merge: function () {
|
||||||
|
if (this.item1.id === this.form_data.target.id) {
|
||||||
|
this.makeToast(this.$t("Error"), this.$t("err_merge_self"), "danger")
|
||||||
|
this.$emit("finish-action", "cancel")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.item1.id || !this.form_data.target.id) {
|
||||||
|
this.makeToast(this.$t("Warning"), this.$t("nothing"), "warning")
|
||||||
|
this.$emit("finish-action", "cancel")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.genericAPI(this.model, this.Actions.MERGE, {
|
||||||
|
source: this.item1.id,
|
||||||
|
target: this.form_data.target.id,
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
this.$emit("finish-action", { target: this.form_data.target.id })
|
||||||
|
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_MERGE)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
//TODO error checking not working with OpenAPI methods
|
||||||
|
console.log("Error", err)
|
||||||
|
StandardToasts.makeStandardToast(StandardToasts.FAIL_MERGE)
|
||||||
|
this.$emit("finish-action", "cancel")
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.item1.automate) {
|
||||||
|
let apiClient = new ApiApiFactory()
|
||||||
|
let automation = {
|
||||||
|
name: `Merge ${this.item1.name} with ${this.form_data.target.name}`,
|
||||||
|
param_1: this.item1.name,
|
||||||
|
param_2: this.form_data.target.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.model === this.Models.FOOD) {
|
||||||
|
automation.type = "FOOD_ALIAS"
|
||||||
|
}
|
||||||
|
if (this.model === this.Models.UNIT) {
|
||||||
|
automation.type = "UNIT_ALIAS"
|
||||||
|
}
|
||||||
|
if (this.model === this.Models.KEYWORD) {
|
||||||
|
automation.type = "KEYWORD_ALIAS"
|
||||||
|
}
|
||||||
|
|
||||||
|
apiClient.createAutomation(automation)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,157 +1,171 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<b-form-group
|
<b-form-group class="mb-3">
|
||||||
v-bind:label="form.label"
|
<template #label v-if="show_label">
|
||||||
class="mb-3">
|
{{ form.label }}
|
||||||
<generic-multiselect
|
</template>
|
||||||
@change="new_value=$event.val"
|
<generic-multiselect
|
||||||
@remove="new_value=undefined"
|
@change="new_value = $event.val"
|
||||||
:initial_selection="initialSelection"
|
@remove="new_value = undefined"
|
||||||
:model="model"
|
:initial_selection="initialSelection"
|
||||||
:multiple="useMultiple"
|
:model="model"
|
||||||
:sticky_options="sticky_options"
|
:multiple="useMultiple"
|
||||||
:allow_create="create_new"
|
:sticky_options="sticky_options"
|
||||||
:create_placeholder="createPlaceholder"
|
:allow_create="form.allow_create"
|
||||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
:create_placeholder="createPlaceholder"
|
||||||
:placeholder="modelName"
|
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||||
@new="addNew">
|
:placeholder="modelName"
|
||||||
</generic-multiselect>
|
@new="addNew"
|
||||||
|
>
|
||||||
|
</generic-multiselect>
|
||||||
</b-form-group>
|
</b-form-group>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import GenericMultiselect from "@/components/GenericMultiselect";
|
import GenericMultiselect from "@/components/GenericMultiselect"
|
||||||
import {StandardToasts, ApiMixin} from "@/utils/utils";
|
import { StandardToasts, ApiMixin } from "@/utils/utils"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'LookupInput',
|
name: "LookupInput",
|
||||||
components: {GenericMultiselect},
|
components: { GenericMultiselect },
|
||||||
mixins: [ApiMixin],
|
mixins: [ApiMixin],
|
||||||
props: {
|
props: {
|
||||||
form: {type: Object, default () {return undefined}},
|
form: {
|
||||||
model: {type: Object, default () {return undefined}},
|
type: Object,
|
||||||
|
default() {
|
||||||
// TODO: include create_new and create_text props and associated functionality to create objects for drop down
|
return undefined
|
||||||
// see 'tagging' here: https://vue-multiselect.js.org/#sub-tagging
|
},
|
||||||
// perfect world would have it trigger a new modal associated with the associated item model
|
},
|
||||||
},
|
model: {
|
||||||
data() {
|
type: Object,
|
||||||
return {
|
default() {
|
||||||
new_value: undefined,
|
return undefined
|
||||||
field: undefined,
|
},
|
||||||
label: undefined,
|
},
|
||||||
sticky_options: undefined,
|
show_label: { type: Boolean, default: true },
|
||||||
first_run: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.new_value = this.form?.value
|
|
||||||
this.field = this.form?.field ?? 'You Forgot To Set Field Name'
|
|
||||||
this.label = this.form?.label ?? ''
|
|
||||||
this.sticky_options = this.form?.sticky_options ?? []
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
modelName() {
|
|
||||||
return this?.model?.name ?? this.$t('Search')
|
|
||||||
},
|
},
|
||||||
useMultiple() {
|
data() {
|
||||||
return this.form?.multiple || this.form?.ordered || false
|
return {
|
||||||
},
|
new_value: undefined,
|
||||||
initialSelection() {
|
field: undefined,
|
||||||
let this_value = this.form.value
|
label: undefined,
|
||||||
let arrayValues = undefined
|
sticky_options: undefined,
|
||||||
// multiselect is expect to get an array of objects - make sure it gets one
|
first_run: true,
|
||||||
if (Array.isArray(this_value)) {
|
|
||||||
arrayValues = this_value
|
|
||||||
} else if (!this_value) {
|
|
||||||
arrayValues = []
|
|
||||||
} else if (typeof(this_value) === 'object') {
|
|
||||||
arrayValues = [this_value]
|
|
||||||
} else {
|
|
||||||
arrayValues = [{'id': -1, 'name': this_value}]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.form?.ordered && this.first_run) {
|
|
||||||
return this.flattenItems(arrayValues)
|
|
||||||
} else {
|
|
||||||
return arrayValues
|
|
||||||
}
|
|
||||||
},
|
|
||||||
createPlaceholder() {
|
|
||||||
return this.$t('Create_New_' + this?.model?.name)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
'new_value': function () {
|
|
||||||
let x = this?.new_value
|
|
||||||
// pass the unflattened attributes that can be restored when ready to save/update
|
|
||||||
if (this.form?.ordered) {
|
|
||||||
x['__override__'] = this.unflattenItem(this?.new_value)
|
|
||||||
}
|
|
||||||
this.$root.$emit('change', this.form.field, x)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
addNew: function(e) {
|
|
||||||
// if create a new item requires more than 1 parameter or the field 'name' is insufficient this will need reworked
|
|
||||||
// in a perfect world this would trigger a new modal and allow editing all fields
|
|
||||||
this.genericAPI(this.model, this.Actions.CREATE, {'name': e}).then((result) => {
|
|
||||||
this.new_value = result.data
|
|
||||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
|
||||||
}).catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
// ordered lookups have nested attributes that need flattened attributes to drive lookup
|
|
||||||
flattenItems: function(itemlist) {
|
|
||||||
let flat_items = []
|
|
||||||
let item = undefined
|
|
||||||
let label = this.form.list_label.split('::')
|
|
||||||
itemlist.forEach(x => {
|
|
||||||
item = {}
|
|
||||||
for (const [k, v] of Object.entries(x)) {
|
|
||||||
if (k == label[0]) {
|
|
||||||
item['id'] = v.id
|
|
||||||
item[label[1]] = v[label[1]]
|
|
||||||
} else {
|
|
||||||
item[this.form.field + '__' + k] = v
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
flat_items.push(item)
|
|
||||||
});
|
|
||||||
this.first_run = false
|
|
||||||
return flat_items
|
|
||||||
},
|
},
|
||||||
unflattenItem: function(itemList) {
|
mounted() {
|
||||||
let unflat_items = []
|
this.new_value = this.form?.value
|
||||||
let item = undefined
|
this.field = this.form?.field ?? "You Forgot To Set Field Name"
|
||||||
let this_label = undefined
|
this.label = this.form?.label ?? ""
|
||||||
let label = this.form.list_label.split('::')
|
this.sticky_options = this.form?.sticky_options ?? []
|
||||||
let order = 0
|
},
|
||||||
itemList.forEach(x => {
|
computed: {
|
||||||
item = {}
|
modelName() {
|
||||||
item[label[0]] = {}
|
return this?.model?.name ?? this.$t("Search")
|
||||||
for (const [k, v] of Object.entries(x)) {
|
},
|
||||||
switch(k) {
|
useMultiple() {
|
||||||
case 'id':
|
return this.form?.multiple || this.form?.ordered || false
|
||||||
item[label[0]]['id'] = v
|
},
|
||||||
break;
|
initialSelection() {
|
||||||
case label[1]:
|
let this_value = this.new_value
|
||||||
item[label[0]][label[1]] = v
|
let arrayValues = undefined
|
||||||
break;
|
// multiselect is expect to get an array of objects - make sure it gets one
|
||||||
default:
|
if (Array.isArray(this_value)) {
|
||||||
this_label = k.replace(this.form.field + '__', '')
|
arrayValues = this_value
|
||||||
}
|
} else if (!this_value) {
|
||||||
|
arrayValues = []
|
||||||
}
|
} else if (typeof this_value === "object") {
|
||||||
item['order'] = order
|
arrayValues = [this_value]
|
||||||
order++
|
} else {
|
||||||
unflat_items.push(item)
|
arrayValues = [{ id: -1, name: this_value }]
|
||||||
});
|
}
|
||||||
return unflat_items
|
|
||||||
}
|
if (this.form?.ordered && this.first_run) {
|
||||||
}
|
return this.flattenItems(arrayValues)
|
||||||
|
} else {
|
||||||
|
return arrayValues
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createPlaceholder() {
|
||||||
|
return this.$t("Create_New_" + this?.model?.name)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
"form.value": function (newVal, oldVal) {
|
||||||
|
this.new_value = newVal
|
||||||
|
},
|
||||||
|
new_value: function () {
|
||||||
|
let x = this?.new_value
|
||||||
|
// pass the unflattened attributes that can be restored when ready to save/update
|
||||||
|
if (this.form?.ordered) {
|
||||||
|
x["__override__"] = this.unflattenItem(this?.new_value)
|
||||||
|
}
|
||||||
|
this.$root.$emit("change", this.form.field, x)
|
||||||
|
this.$emit("change", x)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
addNew: function (e) {
|
||||||
|
// if create a new item requires more than 1 parameter or the field 'name' is insufficient this will need reworked
|
||||||
|
// in a perfect world this would trigger a new modal and allow editing all fields
|
||||||
|
this.genericAPI(this.model, this.Actions.CREATE, { name: e })
|
||||||
|
.then((result) => {
|
||||||
|
this.new_value = result.data
|
||||||
|
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// ordered lookups have nested attributes that need flattened attributes to drive lookup
|
||||||
|
flattenItems: function (itemlist) {
|
||||||
|
let flat_items = []
|
||||||
|
let item = undefined
|
||||||
|
let label = this.form.list_label.split("::")
|
||||||
|
itemlist.forEach((x) => {
|
||||||
|
item = {}
|
||||||
|
for (const [k, v] of Object.entries(x)) {
|
||||||
|
if (k == label[0]) {
|
||||||
|
item["id"] = v.id
|
||||||
|
item[label[1]] = v[label[1]]
|
||||||
|
} else {
|
||||||
|
item[this.form.field + "__" + k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flat_items.push(item)
|
||||||
|
})
|
||||||
|
this.first_run = false
|
||||||
|
return flat_items
|
||||||
|
},
|
||||||
|
unflattenItem: function (itemList) {
|
||||||
|
let unflat_items = []
|
||||||
|
let item = undefined
|
||||||
|
let this_label = undefined
|
||||||
|
let label = this.form.list_label.split("::")
|
||||||
|
let order = 0
|
||||||
|
itemList.forEach((x) => {
|
||||||
|
item = {}
|
||||||
|
item[label[0]] = {}
|
||||||
|
for (const [k, v] of Object.entries(x)) {
|
||||||
|
switch (k) {
|
||||||
|
case "id":
|
||||||
|
item[label[0]]["id"] = v
|
||||||
|
break
|
||||||
|
case label[1]:
|
||||||
|
item[label[0]][label[1]] = v
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
this_label = k.replace(this.form.field + "__", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item["order"] = order
|
||||||
|
order++
|
||||||
|
unflat_items.push(item)
|
||||||
|
})
|
||||||
|
return unflat_items
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -4,10 +4,14 @@
|
|||||||
"err_creating_resource": "There was an error creating a resource!",
|
"err_creating_resource": "There was an error creating a resource!",
|
||||||
"err_updating_resource": "There was an error updating a resource!",
|
"err_updating_resource": "There was an error updating a resource!",
|
||||||
"err_deleting_resource": "There was an error deleting a resource!",
|
"err_deleting_resource": "There was an error deleting a resource!",
|
||||||
|
"err_moving_resource": "There was an error moving a resource!",
|
||||||
|
"err_merging_resource": "There was an error merging a resource!",
|
||||||
"success_fetching_resource": "Successfully fetched a resource!",
|
"success_fetching_resource": "Successfully fetched a resource!",
|
||||||
"success_creating_resource": "Successfully created a resource!",
|
"success_creating_resource": "Successfully created a resource!",
|
||||||
"success_updating_resource": "Successfully updated a resource!",
|
"success_updating_resource": "Successfully updated a resource!",
|
||||||
"success_deleting_resource": "Successfully deleted a resource!",
|
"success_deleting_resource": "Successfully deleted a resource!",
|
||||||
|
"success_moving_resource": "Successfully moved a resource!",
|
||||||
|
"success_merging_resource": "Successfully merged a resource!",
|
||||||
"file_upload_disabled": "File upload is not enabled for your space.",
|
"file_upload_disabled": "File upload is not enabled for your space.",
|
||||||
"step_time_minutes": "Step time in minutes",
|
"step_time_minutes": "Step time in minutes",
|
||||||
"confirm_delete": "Are you sure you want to delete this {object}?",
|
"confirm_delete": "Are you sure you want to delete this {object}?",
|
||||||
@ -207,5 +211,8 @@
|
|||||||
"New_Cookbook": "New cookbook",
|
"New_Cookbook": "New cookbook",
|
||||||
"Hide_Keyword": "Hide keywords",
|
"Hide_Keyword": "Hide keywords",
|
||||||
"Clear": "Clear",
|
"Clear": "Clear",
|
||||||
|
"err_move_self": "Cannot move item to itself",
|
||||||
|
"nothing": "Nothing to do",
|
||||||
|
"err_merge_self": "Cannot merge item with itself",
|
||||||
"show_sql": "Show SQL"
|
"show_sql": "Show SQL"
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,15 +1,26 @@
|
|||||||
/*
|
/*
|
||||||
* Utility functions to call bootstrap toasts
|
* Utility functions to call bootstrap toasts
|
||||||
* */
|
* */
|
||||||
import {BToast} from 'bootstrap-vue'
|
import i18n from "@/i18n"
|
||||||
import i18n from "@/i18n";
|
import { frac } from "@/utils/fractions"
|
||||||
|
/*
|
||||||
|
* Utility functions to use OpenAPIs generically
|
||||||
|
* */
|
||||||
|
import { ApiApiFactory } from "@/utils/openapi/api.ts"
|
||||||
|
import axios from "axios"
|
||||||
|
import { BToast } from "bootstrap-vue"
|
||||||
|
// /*
|
||||||
|
// * Utility functions to use manipulate nested components
|
||||||
|
// * */
|
||||||
|
import Vue from "vue"
|
||||||
|
import { Actions, Models } from "./models"
|
||||||
|
|
||||||
export const ToastMixin = {
|
export const ToastMixin = {
|
||||||
methods: {
|
methods: {
|
||||||
makeToast: function (title, message, variant = null) {
|
makeToast: function (title, message, variant = null) {
|
||||||
return makeToast(title, message, variant)
|
return makeToast(title, message, variant)
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeToast(title, message, variant = null) {
|
export function makeToast(title, message, variant = null) {
|
||||||
@ -17,57 +28,71 @@ export function makeToast(title, message, variant = null) {
|
|||||||
toaster.$bvToast.toast(message, {
|
toaster.$bvToast.toast(message, {
|
||||||
title: title,
|
title: title,
|
||||||
variant: variant,
|
variant: variant,
|
||||||
toaster: 'b-toaster-bottom-right',
|
toaster: "b-toaster-bottom-right",
|
||||||
solid: true
|
solid: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StandardToasts {
|
export class StandardToasts {
|
||||||
static SUCCESS_CREATE = 'SUCCESS_CREATE'
|
static SUCCESS_CREATE = "SUCCESS_CREATE"
|
||||||
static SUCCESS_FETCH = 'SUCCESS_FETCH'
|
static SUCCESS_FETCH = "SUCCESS_FETCH"
|
||||||
static SUCCESS_UPDATE = 'SUCCESS_UPDATE'
|
static SUCCESS_UPDATE = "SUCCESS_UPDATE"
|
||||||
static SUCCESS_DELETE = 'SUCCESS_DELETE'
|
static SUCCESS_DELETE = "SUCCESS_DELETE"
|
||||||
|
static SUCCESS_MOVE = "SUCCESS_MOVE"
|
||||||
|
static SUCCESS_MERGE = "SUCCESS_MERGE"
|
||||||
|
|
||||||
static FAIL_CREATE = 'FAIL_CREATE'
|
static FAIL_CREATE = "FAIL_CREATE"
|
||||||
static FAIL_FETCH = 'FAIL_FETCH'
|
static FAIL_FETCH = "FAIL_FETCH"
|
||||||
static FAIL_UPDATE = 'FAIL_UPDATE'
|
static FAIL_UPDATE = "FAIL_UPDATE"
|
||||||
static FAIL_DELETE = 'FAIL_DELETE'
|
static FAIL_DELETE = "FAIL_DELETE"
|
||||||
|
static FAIL_MOVE = "FAIL_MOVE"
|
||||||
|
static FAIL_MERGE = "FAIL_MERGE"
|
||||||
|
|
||||||
static makeStandardToast(toast) {
|
static makeStandardToast(toast, err_details = undefined) {
|
||||||
switch (toast) {
|
switch (toast) {
|
||||||
case StandardToasts.SUCCESS_CREATE:
|
case StandardToasts.SUCCESS_CREATE:
|
||||||
makeToast(i18n.tc('Success'), i18n.tc('success_creating_resource'), 'success')
|
makeToast(i18n.tc("Success"), i18n.tc("success_creating_resource"), "success")
|
||||||
break;
|
break
|
||||||
case StandardToasts.SUCCESS_FETCH:
|
case StandardToasts.SUCCESS_FETCH:
|
||||||
makeToast(i18n.tc('Success'), i18n.tc('success_fetching_resource'), 'success')
|
makeToast(i18n.tc("Success"), i18n.tc("success_fetching_resource"), "success")
|
||||||
break;
|
break
|
||||||
case StandardToasts.SUCCESS_UPDATE:
|
case StandardToasts.SUCCESS_UPDATE:
|
||||||
makeToast(i18n.tc('Success'), i18n.tc('success_updating_resource'), 'success')
|
makeToast(i18n.tc("Success"), i18n.tc("success_updating_resource"), "success")
|
||||||
break;
|
break
|
||||||
case StandardToasts.SUCCESS_DELETE:
|
case StandardToasts.SUCCESS_DELETE:
|
||||||
makeToast(i18n.tc('Success'), i18n.tc('success_deleting_resource'), 'success')
|
makeToast(i18n.tc("Success"), i18n.tc("success_deleting_resource"), "success")
|
||||||
break;
|
break
|
||||||
|
case StandardToasts.SUCCESS_MOVE:
|
||||||
|
makeToast(i18n.tc("Success"), i18n.tc("success_moving_resource"), "success")
|
||||||
|
break
|
||||||
|
case StandardToasts.SUCCESS_MERGE:
|
||||||
|
makeToast(i18n.tc("Success"), i18n.tc("success_merging_resource"), "success")
|
||||||
|
break
|
||||||
case StandardToasts.FAIL_CREATE:
|
case StandardToasts.FAIL_CREATE:
|
||||||
makeToast(i18n.tc('Failure'), i18n.tc('err_creating_resource'), 'danger')
|
makeToast(i18n.tc("Failure"), i18n.tc("err_creating_resource"), "danger")
|
||||||
break;
|
break
|
||||||
case StandardToasts.FAIL_FETCH:
|
case StandardToasts.FAIL_FETCH:
|
||||||
makeToast(i18n.tc('Failure'), i18n.tc('err_fetching_resource'), 'danger')
|
makeToast(i18n.tc("Failure"), i18n.tc("err_fetching_resource"), "danger")
|
||||||
break;
|
break
|
||||||
case StandardToasts.FAIL_UPDATE:
|
case StandardToasts.FAIL_UPDATE:
|
||||||
makeToast(i18n.tc('Failure'), i18n.tc('err_updating_resource'), 'danger')
|
makeToast(i18n.tc("Failure"), i18n.tc("err_updating_resource"), "danger")
|
||||||
break;
|
break
|
||||||
case StandardToasts.FAIL_DELETE:
|
case StandardToasts.FAIL_DELETE:
|
||||||
makeToast(i18n.tc('Failure'), i18n.tc('err_deleting_resource'), 'danger')
|
makeToast(i18n.tc("Failure"), i18n.tc("err_deleting_resource"), "danger")
|
||||||
break;
|
break
|
||||||
|
case StandardToasts.FAIL_MOVE:
|
||||||
|
makeToast(i18n.tc("Failure"), i18n.tc("err_moving_resource") + (err_details ? "\n" + err_details : ""), "danger")
|
||||||
|
break
|
||||||
|
case StandardToasts.FAIL_MERGE:
|
||||||
|
makeToast(i18n.tc("Failure"), i18n.tc("err_merging_resource") + (err_details ? "\n" + err_details : ""), "danger")
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Utility functions to use djangos gettext
|
* Utility functions to use djangos gettext
|
||||||
* */
|
* */
|
||||||
|
|
||||||
export const GettextMixin = {
|
export const GettextMixin = {
|
||||||
methods: {
|
methods: {
|
||||||
@ -77,8 +102,8 @@ export const GettextMixin = {
|
|||||||
*/
|
*/
|
||||||
_: function (param) {
|
_: function (param) {
|
||||||
return djangoGettext(param)
|
return djangoGettext(param)
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export function djangoGettext(param) {
|
export function djangoGettext(param) {
|
||||||
@ -86,8 +111,8 @@ export function djangoGettext(param) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Utility function to use djangos named urls
|
* Utility function to use djangos named urls
|
||||||
* */
|
* */
|
||||||
|
|
||||||
// uses https://github.com/ierror/django-js-reverse#use-the-urls-in-javascript
|
// uses https://github.com/ierror/django-js-reverse#use-the-urls-in-javascript
|
||||||
export const ResolveUrlMixin = {
|
export const ResolveUrlMixin = {
|
||||||
@ -99,50 +124,48 @@ export const ResolveUrlMixin = {
|
|||||||
*/
|
*/
|
||||||
resolveDjangoUrl: function (url, params = null) {
|
resolveDjangoUrl: function (url, params = null) {
|
||||||
return resolveDjangoUrl(url, params)
|
return resolveDjangoUrl(url, params)
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveDjangoUrl(url, params = null) {
|
export function resolveDjangoUrl(url, params = null) {
|
||||||
if (params == null) {
|
if (params == null) {
|
||||||
return window.Urls[url]()
|
return window.Urls[url]()
|
||||||
} else if (typeof(params) != "object") {
|
} else if (typeof params != "object") {
|
||||||
return window.Urls[url](params)
|
return window.Urls[url](params)
|
||||||
} else if (typeof(params) == "object") {
|
} else if (typeof params == "object") {
|
||||||
if (params.length === 1) {
|
if (params.length === 1) {
|
||||||
return window.Urls[url](params)
|
return window.Urls[url](params)
|
||||||
} else if (params.length === 2) {
|
} else if (params.length === 2) {
|
||||||
return window.Urls[url](params[0],params[1])
|
return window.Urls[url](params[0], params[1])
|
||||||
} else if (params.length === 3) {
|
} else if (params.length === 3) {
|
||||||
return window.Urls[url](params[0],params[1],params[2])
|
return window.Urls[url](params[0], params[1], params[2])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* other utilities
|
* other utilities
|
||||||
* */
|
* */
|
||||||
|
|
||||||
export function getUserPreference(pref) {
|
export function getUserPreference(pref) {
|
||||||
if(window.USER_PREF === undefined) {
|
if (window.USER_PREF === undefined) {
|
||||||
return undefined;
|
return undefined
|
||||||
}
|
}
|
||||||
return window.USER_PREF[pref]
|
return window.USER_PREF[pref]
|
||||||
}
|
}
|
||||||
|
|
||||||
import {frac} from "@/utils/fractions";
|
|
||||||
|
|
||||||
export function calculateAmount(amount, factor) {
|
export function calculateAmount(amount, factor) {
|
||||||
if (getUserPreference('use_fractions')) {
|
if (getUserPreference("use_fractions")) {
|
||||||
let return_string = ''
|
let return_string = ""
|
||||||
let fraction = frac((amount * factor), 10, true)
|
let fraction = frac(amount * factor, 10, true)
|
||||||
|
|
||||||
if (fraction[0] > 0) {
|
if (fraction[0] > 0) {
|
||||||
return_string += fraction[0]
|
return_string += fraction[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fraction[1] > 0) {
|
if (fraction[1] > 0) {
|
||||||
return_string += ` <sup>${(fraction[1])}</sup>⁄<sub>${(fraction[2])}</sub>`
|
return_string += ` <sup>${fraction[1]}</sup>⁄<sub>${fraction[2]}</sub>`
|
||||||
}
|
}
|
||||||
|
|
||||||
return return_string
|
return return_string
|
||||||
@ -152,23 +175,23 @@ export function calculateAmount(amount, factor) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function roundDecimals(num) {
|
export function roundDecimals(num) {
|
||||||
let decimals = ((getUserPreference('user_fractions')) ? getUserPreference('user_fractions') : 2);
|
let decimals = getUserPreference("user_fractions") ? getUserPreference("user_fractions") : 2
|
||||||
return +(Math.round(num + `e+${decimals}`) + `e-${decimals}`);
|
return +(Math.round(num + `e+${decimals}`) + `e-${decimals}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const KILOJOULES_PER_CALORIE = 4.18
|
const KILOJOULES_PER_CALORIE = 4.18
|
||||||
|
|
||||||
export function calculateEnergy(amount, factor) {
|
export function calculateEnergy(amount, factor) {
|
||||||
if (getUserPreference('use_kj')) {
|
if (getUserPreference("use_kj")) {
|
||||||
let joules = amount * KILOJOULES_PER_CALORIE
|
let joules = amount * KILOJOULES_PER_CALORIE
|
||||||
return calculateAmount(joules, factor) + ' kJ'
|
return calculateAmount(joules, factor) + " kJ"
|
||||||
} else {
|
} else {
|
||||||
return calculateAmount(amount, factor) + ' kcal'
|
return calculateAmount(amount, factor) + " kcal"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertEnergyToCalories(amount) {
|
export function convertEnergyToCalories(amount) {
|
||||||
if (getUserPreference('use_kj')) {
|
if (getUserPreference("use_kj")) {
|
||||||
return amount / KILOJOULES_PER_CALORIE
|
return amount / KILOJOULES_PER_CALORIE
|
||||||
} else {
|
} else {
|
||||||
return amount
|
return amount
|
||||||
@ -176,33 +199,25 @@ export function convertEnergyToCalories(amount) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function energyHeading() {
|
export function energyHeading() {
|
||||||
if (getUserPreference('use_kj')) {
|
if (getUserPreference("use_kj")) {
|
||||||
return 'Energy'
|
return "Energy"
|
||||||
} else {
|
} else {
|
||||||
return 'Calories'
|
return "Calories"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
axios.defaults.xsrfCookieName = "csrftoken"
|
||||||
* Utility functions to use OpenAPIs generically
|
|
||||||
* */
|
|
||||||
import {ApiApiFactory} from "@/utils/openapi/api.ts";
|
|
||||||
|
|
||||||
import axios from "axios";
|
|
||||||
axios.defaults.xsrfCookieName = 'csrftoken'
|
|
||||||
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
|
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
|
||||||
import { Actions, Models } from './models';
|
|
||||||
import {RequestArgs} from "@/utils/openapi/base";
|
|
||||||
|
|
||||||
export const ApiMixin = {
|
export const ApiMixin = {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
Models: Models,
|
Models: Models,
|
||||||
Actions: Actions
|
Actions: Actions,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
genericAPI: function(model, action, options) {
|
genericAPI: function (model, action, options) {
|
||||||
let setup = getConfig(model, action)
|
let setup = getConfig(model, action)
|
||||||
if (setup?.config?.function) {
|
if (setup?.config?.function) {
|
||||||
return specialCases[setup.config.function](action, options, setup)
|
return specialCases[setup.config.function](action, options, setup)
|
||||||
@ -212,10 +227,10 @@ export const ApiMixin = {
|
|||||||
let apiClient = new ApiApiFactory()
|
let apiClient = new ApiApiFactory()
|
||||||
return apiClient[func](...parameters)
|
return apiClient[func](...parameters)
|
||||||
},
|
},
|
||||||
genericGetAPI: function(url, options) {
|
genericGetAPI: function (url, options) {
|
||||||
return axios.get(this.resolveDjangoUrl(url), {'params':options, 'emulateJSON': true})
|
return axios.get(this.resolveDjangoUrl(url), { params: options, emulateJSON: true })
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// /*
|
// /*
|
||||||
@ -223,37 +238,37 @@ export const ApiMixin = {
|
|||||||
// * */
|
// * */
|
||||||
function formatParam(config, value, options) {
|
function formatParam(config, value, options) {
|
||||||
if (config) {
|
if (config) {
|
||||||
for (const [k, v] of Object.entries(config)) {
|
for (const [k, v] of Object.entries(config)) {
|
||||||
switch(k) {
|
switch (k) {
|
||||||
case 'type':
|
case "type":
|
||||||
switch(v) {
|
switch (v) {
|
||||||
case 'string':
|
case "string":
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
let tmpValue = []
|
let tmpValue = []
|
||||||
value.forEach(x => tmpValue.push(String(x)))
|
value.forEach((x) => tmpValue.push(String(x)))
|
||||||
value = tmpValue
|
value = tmpValue
|
||||||
} else if (value !== undefined) {
|
} else if (value !== undefined) {
|
||||||
value = String(value)
|
value = String(value)
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
case 'integer':
|
case "integer":
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
let tmpValue = []
|
let tmpValue = []
|
||||||
value.forEach(x => tmpValue.push(parseInt(x)))
|
value.forEach((x) => tmpValue.push(parseInt(x)))
|
||||||
value = tmpValue
|
value = tmpValue
|
||||||
} else if (value !== undefined) {
|
} else if (value !== undefined) {
|
||||||
value = parseInt(value)
|
value = parseInt(value)
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
case 'function':
|
case "function":
|
||||||
// needs wrapped in a promise and wait for the called function to complete before moving on
|
// needs wrapped in a promise and wait for the called function to complete before moving on
|
||||||
specialCases[v](value, options)
|
specialCases[v](value, options)
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
function buildParams(options, setup) {
|
function buildParams(options, setup) {
|
||||||
@ -280,60 +295,56 @@ function buildParams(options, setup) {
|
|||||||
this_value = getDefault(config?.[item], options)
|
this_value = getDefault(config?.[item], options)
|
||||||
}
|
}
|
||||||
parameters.push(this_value)
|
parameters.push(this_value)
|
||||||
});
|
})
|
||||||
return parameters
|
return parameters
|
||||||
}
|
}
|
||||||
function getDefault(config, options) {
|
function getDefault(config, options) {
|
||||||
let value = undefined
|
let value = undefined
|
||||||
value = config?.default ?? undefined
|
value = config?.default ?? undefined
|
||||||
if (typeof(value) === 'object') {
|
if (typeof value === "object") {
|
||||||
let condition = false
|
let condition = false
|
||||||
switch(value.function) {
|
switch (value.function) {
|
||||||
// CONDITIONAL case requires 4 keys:
|
// CONDITIONAL case requires 4 keys:
|
||||||
// - check: which other OPTIONS key to check against
|
// - check: which other OPTIONS key to check against
|
||||||
// - operator: what type of operation to perform
|
// - operator: what type of operation to perform
|
||||||
// - true: what value to assign when true
|
// - true: what value to assign when true
|
||||||
// - false: what value to assign when false
|
// - false: what value to assign when false
|
||||||
case 'CONDITIONAL':
|
case "CONDITIONAL":
|
||||||
switch(value.operator) {
|
switch (value.operator) {
|
||||||
case 'not_exist':
|
case "not_exist":
|
||||||
condition = (
|
condition = (!options?.[value.check] ?? undefined) || options?.[value.check]?.length == 0
|
||||||
(!options?.[value.check] ?? undefined)
|
|
||||||
|| options?.[value.check]?.length == 0
|
|
||||||
)
|
|
||||||
if (condition) {
|
if (condition) {
|
||||||
value = value.true
|
value = value.true
|
||||||
} else {
|
} else {
|
||||||
value = value.false
|
value = value.false
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
export function getConfig(model, action) {
|
export function getConfig(model, action) {
|
||||||
|
|
||||||
let f = action.function
|
let f = action.function
|
||||||
// if not defined partialUpdate will use params from create
|
// if not defined partialUpdate will use params from create
|
||||||
if (f === 'partialUpdate' && !model?.[f]?.params) {
|
if (f === "partialUpdate" && !model?.[f]?.params) {
|
||||||
model[f] = {'params': [...['id'], ...model.create.params]}
|
model[f] = { params: [...["id"], ...model.create.params] }
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = {
|
let config = {
|
||||||
'name': model.name,
|
name: model.name,
|
||||||
'apiName': model.apiName,
|
apiName: model.apiName,
|
||||||
}
|
}
|
||||||
// spread operator merges dictionaries - last item in list takes precedence
|
// spread operator merges dictionaries - last item in list takes precedence
|
||||||
config = {...config, ...action, ...model.model_type?.[f], ...model?.[f]}
|
config = { ...config, ...action, ...model.model_type?.[f], ...model?.[f] }
|
||||||
// nested dictionaries are not merged - so merge again on any nested keys
|
// nested dictionaries are not merged - so merge again on any nested keys
|
||||||
config.config = {...action?.config, ...model.model_type?.[f]?.config, ...model?.[f]?.config}
|
config.config = { ...action?.config, ...model.model_type?.[f]?.config, ...model?.[f]?.config }
|
||||||
// look in partialUpdate again if necessary
|
// look in partialUpdate again if necessary
|
||||||
if (f === 'partialUpdate' && Object.keys(config.config).length === 0) {
|
if (f === "partialUpdate" && Object.keys(config.config).length === 0) {
|
||||||
config.config = {...model.model_type?.create?.config, ...model?.create?.config}
|
config.config = { ...model.model_type?.create?.config, ...model?.create?.config }
|
||||||
}
|
}
|
||||||
config['function'] = f + config.apiName + (config?.suffix ?? '') // parens are required to force optional chaining to evaluate before concat
|
config["function"] = f + config.apiName + (config?.suffix ?? "") // parens are required to force optional chaining to evaluate before concat
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -342,181 +353,175 @@ export function getConfig(model, action) {
|
|||||||
// * */
|
// * */
|
||||||
export function getForm(model, action, item1, item2) {
|
export function getForm(model, action, item1, item2) {
|
||||||
let f = action.function
|
let f = action.function
|
||||||
let config = {...action?.form, ...model.model_type?.[f]?.form, ...model?.[f]?.form}
|
let config = { ...action?.form, ...model.model_type?.[f]?.form, ...model?.[f]?.form }
|
||||||
// if not defined partialUpdate will use form from create
|
// if not defined partialUpdate will use form from create
|
||||||
if (f === 'partialUpdate' && Object.keys(config).length == 0) {
|
if (f === "partialUpdate" && Object.keys(config).length == 0) {
|
||||||
config = {...Actions.CREATE?.form, ...model.model_type?.['create']?.form, ...model?.['create']?.form}
|
config = { ...Actions.CREATE?.form, ...model.model_type?.["create"]?.form, ...model?.["create"]?.form }
|
||||||
config['title'] = {...action?.form_title, ...model.model_type?.[f]?.form_title, ...model?.[f]?.form_title}
|
config["title"] = { ...action?.form_title, ...model.model_type?.[f]?.form_title, ...model?.[f]?.form_title }
|
||||||
}
|
}
|
||||||
let form = {'fields': []}
|
let form = { fields: [] }
|
||||||
let value = ''
|
let value = ""
|
||||||
for (const [k, v] of Object.entries(config)) {
|
for (const [k, v] of Object.entries(config)) {
|
||||||
if (v?.function){
|
if (v?.function) {
|
||||||
switch(v.function) {
|
switch (v.function) {
|
||||||
case 'translate':
|
case "translate":
|
||||||
value = formTranslate(v, model, item1, item2)
|
value = formTranslate(v, model, item1, item2)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
value = v
|
value = v
|
||||||
}
|
}
|
||||||
if (value?.form_field) {
|
if (value?.form_field) {
|
||||||
value['value'] = item1?.[value?.field] ?? undefined
|
value["value"] = item1?.[value?.field] ?? undefined
|
||||||
form.fields.push(
|
form.fields.push({
|
||||||
{
|
...value,
|
||||||
...value,
|
...{
|
||||||
...{
|
label: formTranslate(value?.label, model, item1, item2),
|
||||||
'label': formTranslate(value?.label, model, item1, item2),
|
placeholder: formTranslate(value?.placeholder, model, item1, item2),
|
||||||
'placeholder': formTranslate(value?.placeholder, model, item1, item2)
|
},
|
||||||
}
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
form[k] = value
|
form[k] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return form
|
return form
|
||||||
|
|
||||||
}
|
}
|
||||||
function formTranslate(translate, model, item1, item2) {
|
function formTranslate(translate, model, item1, item2) {
|
||||||
if (typeof(translate) !== 'object') {return translate}
|
if (typeof translate !== "object") {
|
||||||
|
return translate
|
||||||
|
}
|
||||||
let phrase = translate.phrase
|
let phrase = translate.phrase
|
||||||
let options = {}
|
let options = {}
|
||||||
let obj = undefined
|
let obj = undefined
|
||||||
translate?.params.forEach(function (x, index) {
|
translate?.params.forEach(function (x, index) {
|
||||||
switch(x.from){
|
switch (x.from) {
|
||||||
case 'item1':
|
case "item1":
|
||||||
obj = item1
|
obj = item1
|
||||||
break;
|
break
|
||||||
case 'item2':
|
case "item2":
|
||||||
obj = item2
|
obj = item2
|
||||||
break;
|
break
|
||||||
case 'model':
|
case "model":
|
||||||
obj = model
|
obj = model
|
||||||
}
|
}
|
||||||
options[x.token] = obj[x.attribute]
|
options[x.token] = obj[x.attribute]
|
||||||
})
|
})
|
||||||
return i18n.t(phrase, options)
|
return i18n.t(phrase, options)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// /*
|
|
||||||
// * Utility functions to use manipulate nested components
|
|
||||||
// * */
|
|
||||||
import Vue from 'vue'
|
|
||||||
export const CardMixin = {
|
export const CardMixin = {
|
||||||
methods: {
|
methods: {
|
||||||
findCard: function(id, card_list){
|
findCard: function (id, card_list) {
|
||||||
let card_length = card_list?.length ?? 0
|
let card_length = card_list?.length ?? 0
|
||||||
if (card_length == 0) {
|
if (card_length == 0) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
let cards = card_list.filter(obj => obj.id == id)
|
let cards = card_list.filter((obj) => obj.id == id)
|
||||||
if (cards.length == 1) {
|
if (cards.length == 1) {
|
||||||
return cards[0]
|
return cards[0]
|
||||||
} else if (cards.length == 0) {
|
} else if (cards.length == 0) {
|
||||||
for (const c of card_list.filter(x => x.show_children == true)) {
|
for (const c of card_list.filter((x) => x.show_children == true)) {
|
||||||
cards = this.findCard(id, c.children)
|
cards = this.findCard(id, c.children)
|
||||||
if (cards) {
|
if (cards) {
|
||||||
return cards
|
return cards
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.log('something terrible happened')
|
console.log("something terrible happened")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
destroyCard: function(id, card_list) {
|
destroyCard: function (id, card_list) {
|
||||||
let card = this.findCard(id, card_list)
|
let card = this.findCard(id, card_list)
|
||||||
let p_id = card?.parent ?? undefined
|
let p_id = card?.parent ?? undefined
|
||||||
|
|
||||||
if (p_id) {
|
if (p_id) {
|
||||||
let parent = this.findCard(p_id, card_list)
|
let parent = this.findCard(p_id, card_list)
|
||||||
if (parent){
|
if (parent) {
|
||||||
Vue.set(parent, 'numchild', parent.numchild - 1)
|
Vue.set(parent, "numchild", parent.numchild - 1)
|
||||||
if (parent.show_children) {
|
if (parent.show_children) {
|
||||||
let idx = parent.children.indexOf(parent.children.find(x => x.id === id))
|
let idx = parent.children.indexOf(parent.children.find((x) => x.id === id))
|
||||||
Vue.delete(parent.children, idx)
|
Vue.delete(parent.children, idx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return card_list.filter(x => x.id != id)
|
return card_list.filter((x) => x.id != id)
|
||||||
},
|
},
|
||||||
refreshCard: function(obj, card_list){
|
refreshCard: function (obj, card_list) {
|
||||||
let target = {}
|
let target = {}
|
||||||
let idx = undefined
|
let idx = undefined
|
||||||
target = this.findCard(obj.id, card_list)
|
target = this.findCard(obj.id, card_list)
|
||||||
|
|
||||||
if (target) {
|
if (target) {
|
||||||
idx = card_list.indexOf(card_list.find(x => x.id === target.id))
|
idx = card_list.indexOf(card_list.find((x) => x.id === target.id))
|
||||||
Vue.set(card_list, idx, obj)
|
Vue.set(card_list, idx, obj)
|
||||||
}
|
}
|
||||||
if (target?.parent) {
|
if (target?.parent) {
|
||||||
let parent = this.findCard(target.parent, card_list)
|
let parent = this.findCard(target.parent, card_list)
|
||||||
if (parent) {
|
if (parent) {
|
||||||
if (parent.show_children){
|
if (parent.show_children) {
|
||||||
idx = parent.children.indexOf(parent.children.find(x => x.id === target.id))
|
idx = parent.children.indexOf(parent.children.find((x) => x.id === target.id))
|
||||||
Vue.set(parent.children, idx, obj)
|
Vue.set(parent.children, idx, obj)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const specialCases = {
|
const specialCases = {
|
||||||
// the supermarket API requires chaining promises together, instead of trying to make
|
// the supermarket API requires chaining promises together, instead of trying to make
|
||||||
// this use case generic just treat it as a unique use case
|
// this use case generic just treat it as a unique use case
|
||||||
SupermarketWithCategories: function(action, options, setup) {
|
SupermarketWithCategories: function (action, options, setup) {
|
||||||
let API = undefined
|
let API = undefined
|
||||||
let GenericAPI = ApiMixin.methods.genericAPI
|
let GenericAPI = ApiMixin.methods.genericAPI
|
||||||
let params = []
|
let params = []
|
||||||
if (action.function === 'partialUpdate') {
|
if (action.function === "partialUpdate") {
|
||||||
API = GenericAPI
|
API = GenericAPI
|
||||||
params = [Models.SUPERMARKET, Actions.FETCH, {'id': options.id}]
|
params = [Models.SUPERMARKET, Actions.FETCH, { id: options.id }]
|
||||||
|
} else if (action.function === "create") {
|
||||||
} else if (action.function === 'create') {
|
|
||||||
API = new ApiApiFactory()[setup.function]
|
API = new ApiApiFactory()[setup.function]
|
||||||
params = buildParams(options, setup)
|
params = buildParams(options, setup)
|
||||||
}
|
}
|
||||||
|
|
||||||
return API(...params).then((result) => {
|
return API(...params)
|
||||||
// either get the supermarket or create the supermarket (but without the category relations)
|
.then((result) => {
|
||||||
return result.data
|
// either get the supermarket or create the supermarket (but without the category relations)
|
||||||
}).then((result) => {
|
return result.data
|
||||||
// delete, update or change all of the category/relations
|
|
||||||
let id = result.id
|
|
||||||
let existing_categories = result.category_to_supermarket
|
|
||||||
let updated_categories = options.category_to_supermarket
|
|
||||||
|
|
||||||
let promises = []
|
|
||||||
// if the 'category.name' key does not exist on the updated_categories, the categories were not updated
|
|
||||||
if (updated_categories?.[0]?.category?.name) {
|
|
||||||
// list of category relationship ids that are not part of the updated supermarket
|
|
||||||
let removed_categories = existing_categories.filter(x => !updated_categories.map(x => x.category.id).includes(x.category.id))
|
|
||||||
let added_categories = updated_categories.filter(x => !existing_categories.map(x => x.category.id).includes(x.category.id))
|
|
||||||
let changed_categories = updated_categories.filter(x => existing_categories.map(x => x.category.id).includes(x.category.id))
|
|
||||||
|
|
||||||
removed_categories.forEach(x => {
|
|
||||||
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.DELETE, {'id': x.id}))
|
|
||||||
})
|
|
||||||
let item = {'supermarket': id}
|
|
||||||
added_categories.forEach(x => {
|
|
||||||
item.order = x.order
|
|
||||||
item.category = {'id': x.category.id, 'name': x.category.name}
|
|
||||||
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.CREATE, item))
|
|
||||||
})
|
|
||||||
changed_categories.forEach(x => {
|
|
||||||
item.id = x?.id ?? existing_categories.find(y => y.category.id === x.category.id).id;
|
|
||||||
item.order = x.order
|
|
||||||
item.category = {'id': x.category.id, 'name': x.category.name}
|
|
||||||
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.UPDATE, item))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.all(promises).then(() => {
|
|
||||||
// finally get and return the Supermarket which everything downstream is expecting
|
|
||||||
return GenericAPI(Models.SUPERMARKET, Actions.FETCH, {'id': id})
|
|
||||||
})
|
})
|
||||||
})
|
.then((result) => {
|
||||||
}
|
// delete, update or change all of the category/relations
|
||||||
|
let id = result.id
|
||||||
|
let existing_categories = result.category_to_supermarket
|
||||||
|
let updated_categories = options.category_to_supermarket
|
||||||
|
|
||||||
|
let promises = []
|
||||||
|
// if the 'category.name' key does not exist on the updated_categories, the categories were not updated
|
||||||
|
if (updated_categories?.[0]?.category?.name) {
|
||||||
|
// list of category relationship ids that are not part of the updated supermarket
|
||||||
|
let removed_categories = existing_categories.filter((x) => !updated_categories.map((x) => x.category.id).includes(x.category.id))
|
||||||
|
let added_categories = updated_categories.filter((x) => !existing_categories.map((x) => x.category.id).includes(x.category.id))
|
||||||
|
let changed_categories = updated_categories.filter((x) => existing_categories.map((x) => x.category.id).includes(x.category.id))
|
||||||
|
|
||||||
|
removed_categories.forEach((x) => {
|
||||||
|
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.DELETE, { id: x.id }))
|
||||||
|
})
|
||||||
|
let item = { supermarket: id }
|
||||||
|
added_categories.forEach((x) => {
|
||||||
|
item.order = x.order
|
||||||
|
item.category = { id: x.category.id, name: x.category.name }
|
||||||
|
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.CREATE, item))
|
||||||
|
})
|
||||||
|
changed_categories.forEach((x) => {
|
||||||
|
item.id = x?.id ?? existing_categories.find((y) => y.category.id === x.category.id).id
|
||||||
|
item.order = x.order
|
||||||
|
item.category = { id: x.category.id, name: x.category.name }
|
||||||
|
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.UPDATE, item))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(promises).then(() => {
|
||||||
|
// finally get and return the Supermarket which everything downstream is expecting
|
||||||
|
return GenericAPI(Models.SUPERMARKET, Actions.FETCH, { id: id })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user