Merge pull request #1086 from smilerz/generic_modal_v2

generic modal refactored
This commit is contained in:
vabene1111 2021-11-30 17:23:33 +01:00 committed by GitHub
commit adc65baf9c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1715 additions and 1566 deletions

View File

@ -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)

View File

@ -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

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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

View File

@ -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],

View File

@ -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):

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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>&frasl;<sub>${(fraction[2])}</sub>` return_string += ` <sup>${fraction[1]}</sup>&frasl;<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 })
})
})
},
} }