Merge pull request #1086 from smilerz/generic_modal_v2
generic modal refactored
This commit is contained in:
commit
adc65baf9c
@ -2,25 +2,30 @@ import operator
|
||||
import pathlib
|
||||
import re
|
||||
import uuid
|
||||
from collections import OrderedDict
|
||||
from datetime import date, timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
from annoying.fields import AutoOneToOneField
|
||||
from django.contrib import auth
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.contrib.postgres.indexes import GinIndex
|
||||
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.db import models, IntegrityError
|
||||
from django.db.models import Index, ProtectedError
|
||||
from django.db import IntegrityError, models
|
||||
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.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 recipes.settings import (COMMENT_PREF_DEFAULT, FRACTION_PREF_DEFAULT,
|
||||
KJ_PREF_DEFAULT, STICKY_NAV_PREF_DEFAULT,
|
||||
SORT_TREE_BY_NAME)
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
from treebeard.mp_tree import MP_Node, MP_NodeManager
|
||||
|
||||
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):
|
||||
@ -38,15 +43,26 @@ def get_model_name(model):
|
||||
|
||||
|
||||
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
|
||||
def get_or_create(self, **kwargs):
|
||||
def get_or_create(self, *args, **kwargs):
|
||||
kwargs['name'] = kwargs['name'].strip()
|
||||
try:
|
||||
return self.get(name__exact=kwargs['name'], space=kwargs['space']), False
|
||||
except self.model.DoesNotExist:
|
||||
with scopes_disabled():
|
||||
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:
|
||||
if 'Key (path)' in e.args[0]:
|
||||
self.model.fix_tree(fix_paths=True)
|
||||
|
@ -2,78 +2,29 @@ from rest_framework.schemas.openapi import AutoSchema
|
||||
from rest_framework.schemas.utils import is_list_view
|
||||
|
||||
|
||||
# TODO move to separate class to cleanup
|
||||
class RecipeSchema(AutoSchema):
|
||||
class QueryParam(object):
|
||||
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):
|
||||
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.append({
|
||||
"name": 'query', "in": "query", "required": False,
|
||||
"description": 'Query string matched (fuzzy) against recipe name. In the future also fulltext search.',
|
||||
'schema': {'type': 'string', },
|
||||
})
|
||||
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', },
|
||||
})
|
||||
for q in self.view.query_params:
|
||||
parameters.append({
|
||||
"name": q.name, "in": "query", "required": q.required,
|
||||
"description": q.description,
|
||||
'schema': {'type': q.qtype, },
|
||||
})
|
||||
|
||||
return parameters
|
||||
|
||||
|
||||
@ -118,15 +69,15 @@ class FilterSchema(AutoSchema):
|
||||
return parameters
|
||||
|
||||
|
||||
class QueryOnlySchema(AutoSchema):
|
||||
def get_path_parameters(self, path, method):
|
||||
if not is_list_view(path, method, self.view):
|
||||
return super(QueryOnlySchema, self).get_path_parameters(path, method)
|
||||
# class QueryOnlySchema(AutoSchema):
|
||||
# def get_path_parameters(self, path, method):
|
||||
# if not is_list_view(path, method, self.view):
|
||||
# return super(QueryOnlySchema, self).get_path_parameters(path, method)
|
||||
|
||||
parameters = super().get_path_parameters(path, method)
|
||||
parameters.append({
|
||||
"name": 'query', "in": "query", "required": False,
|
||||
"description": 'Query string matched (fuzzy) against object name.',
|
||||
'schema': {'type': 'string', },
|
||||
})
|
||||
return parameters
|
||||
# parameters = super().get_path_parameters(path, method)
|
||||
# parameters.append({
|
||||
# "name": 'query', "in": "query", "required": False,
|
||||
# "description": 'Query string matched (fuzzy) against object name.',
|
||||
# 'schema': {'type': 'string', },
|
||||
# })
|
||||
# return parameters
|
||||
|
@ -336,6 +336,10 @@
|
||||
{% block content_fluid %}
|
||||
{% endblock %}
|
||||
|
||||
{% user_prefs request as prefs%}
|
||||
{{ prefs|json_script:'user_preference' }}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{% block script %}
|
||||
|
@ -28,10 +28,10 @@
|
||||
{% trans 'Account' %}</a>
|
||||
</li>
|
||||
<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"
|
||||
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>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
@ -225,4 +225,4 @@
|
||||
window.location.hash = e.target.hash;
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
@ -28,13 +28,6 @@
|
||||
<span class="col col-md-9">
|
||||
<h2>{% trans 'Shopping List' %}</h2>
|
||||
</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">
|
||||
<b-form-checkbox switch size="lg" v-model="edit_mode"
|
||||
@change="$forceUpdate()">{% trans 'Edit' %}</b-form-checkbox>
|
||||
@ -977,4 +970,4 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
@ -1,4 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% comment %} TODO: refactor to be Vue app {% endcomment %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load custom_tags %}
|
||||
|
@ -1,17 +1,19 @@
|
||||
import re
|
||||
from gettext import gettext as _
|
||||
|
||||
import bleach
|
||||
import markdown as md
|
||||
import re
|
||||
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.db.models import Avg
|
||||
from django.templatetags.static import static
|
||||
from django.urls import NoReverseMatch, reverse
|
||||
from recipes import settings
|
||||
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()
|
||||
|
||||
@ -124,10 +126,10 @@ def markdown_link():
|
||||
@register.simple_tag
|
||||
def bookmarklet(request):
|
||||
if request.is_secure():
|
||||
prefix = "https://"
|
||||
protocol = "https://"
|
||||
else:
|
||||
prefix = "http://"
|
||||
server = prefix + request.get_host()
|
||||
protocol = "http://"
|
||||
server = protocol + request.get_host()
|
||||
prefix = settings.JS_REVERSE_SCRIPT_PREFIX
|
||||
# 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:
|
||||
@ -155,3 +157,13 @@ def base_path(request, path_type):
|
||||
return request.META.get('HTTP_X_SCRIPT_NAME', '')
|
||||
elif path_type == 'static_base':
|
||||
return static('vue/manifest.json').replace('vue/manifest.json', '')
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def user_prefs(request):
|
||||
from cookbook.serializer import \
|
||||
UserPreferenceSerializer # putting it with imports caused circular execution
|
||||
try:
|
||||
return UserPreferenceSerializer(request.user.userpreference).data
|
||||
except AttributeError:
|
||||
pass
|
||||
|
@ -1,11 +1,11 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from django.db.models import Subquery, OuterRef
|
||||
from django.db.models import OuterRef, Subquery
|
||||
from django.urls import reverse
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from cookbook.models import Step, Ingredient
|
||||
from cookbook.models import Ingredient, Step
|
||||
|
||||
LIST_URL = 'api:step-list'
|
||||
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_s2.get(reverse(LIST_URL)).content)['results']) == 2
|
||||
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
['a_u', 403],
|
||||
|
@ -36,7 +36,7 @@ from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest, Cus
|
||||
CustomIsShare, CustomIsShared, CustomIsUser,
|
||||
group_required)
|
||||
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.models import (Automation, BookmarkletImport, CookLog, Food, ImportLog, Ingredient,
|
||||
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.local import Local
|
||||
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,
|
||||
CookLogSerializer, FoodSerializer, ImportLogSerializer,
|
||||
IngredientSerializer, KeywordSerializer, MealPlanSerializer,
|
||||
@ -216,7 +218,7 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin):
|
||||
if root.isnumeric():
|
||||
try:
|
||||
root = int(root)
|
||||
except self.model.DoesNotExist:
|
||||
except ValueError:
|
||||
self.queryset = self.model.objects.none()
|
||||
if root == 0:
|
||||
self.queryset = self.model.get_root_nodes()
|
||||
@ -244,7 +246,7 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin):
|
||||
try:
|
||||
child = self.model.objects.get(pk=pk, space=self.request.space)
|
||||
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)
|
||||
|
||||
parent = int(parent)
|
||||
@ -273,7 +275,7 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin):
|
||||
child.move(parent, f'{node_location}-child')
|
||||
content = {'msg': _(f'{child.name} was moved successfully to parent {parent.name}')}
|
||||
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}
|
||||
return Response(content, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@ -497,15 +499,20 @@ class StepViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = StepSerializer
|
||||
permission_classes = [CustomIsUser]
|
||||
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):
|
||||
queryset = self.queryset.filter(recipe__space=self.request.space)
|
||||
|
||||
recipes = self.request.query_params.getlist('recipe', [])
|
||||
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:
|
||||
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):
|
||||
@ -533,8 +540,22 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
||||
# TODO split read and write permission for meal plan guest
|
||||
permission_classes = [CustomIsShare | CustomIsGuest]
|
||||
pagination_class = RecipePagination
|
||||
|
||||
schema = RecipeSchema()
|
||||
# TODO the boolean params below (keywords_or through new) should be updated to boolean types with front end refactored accordingly
|
||||
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):
|
||||
share = self.request.query_params.get('share', None)
|
||||
@ -606,6 +627,15 @@ class ShoppingListEntryViewSet(viewsets.ModelViewSet):
|
||||
queryset = ShoppingListEntry.objects
|
||||
serializer_class = ShoppingListEntrySerializer
|
||||
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):
|
||||
return self.queryset.filter(
|
||||
@ -646,7 +676,7 @@ class ViewLogViewSet(viewsets.ModelViewSet):
|
||||
class CookLogViewSet(viewsets.ModelViewSet):
|
||||
queryset = CookLog.objects
|
||||
serializer_class = CookLogSerializer
|
||||
permission_classes = [CustomIsOwner] # CustomIsShared? since ratings are in the cooklog?
|
||||
permission_classes = [CustomIsOwner]
|
||||
pagination_class = DefaultPagination
|
||||
|
||||
def get_queryset(self):
|
||||
|
@ -1,479 +1,477 @@
|
||||
<template>
|
||||
<div id="app" style="margin-bottom: 4vh" 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"/>
|
||||
<div id="app" style="margin-bottom: 4vh" 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" />
|
||||
|
||||
<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="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">
|
||||
<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>
|
||||
|
||||
<!-- dynamically loaded header components -->
|
||||
<div class="row" v-if="header_component_name !== ''">
|
||||
<div class="col-md-12">
|
||||
<component :is="headerComponent"></component>
|
||||
<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 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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue"
|
||||
import { BootstrapVue } from "bootstrap-vue"
|
||||
|
||||
import Vue from 'vue'
|
||||
import {BootstrapVue} from 'bootstrap-vue'
|
||||
import "bootstrap-vue/dist/bootstrap-vue.css"
|
||||
|
||||
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 {StandardToasts, ToastMixin} from "@/utils/utils";
|
||||
|
||||
import GenericInfiniteCards from "@/components/GenericInfiniteCards";
|
||||
import GenericHorizontalCard from "@/components/GenericHorizontalCard";
|
||||
import GenericModalForm from "@/components/Modals/GenericModalForm";
|
||||
import ModelMenu from "@/components/ModelMenu";
|
||||
import {ApiApiFactory} from "@/utils/openapi/api";
|
||||
import GenericInfiniteCards from "@/components/GenericInfiniteCards"
|
||||
import GenericHorizontalCard from "@/components/GenericHorizontalCard"
|
||||
import GenericModalForm from "@/components/Modals/GenericModalForm"
|
||||
import ModelMenu from "@/components/ModelMenu"
|
||||
import { ApiApiFactory } from "@/utils/openapi/api"
|
||||
//import StorageQuota from "@/components/StorageQuota";
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
export default {
|
||||
// TODO ApiGenerator doesn't capture and share error information - would be nice to share error details when available
|
||||
// or i'm capturing it incorrectly
|
||||
name: 'ModelListView',
|
||||
mixins: [CardMixin, ApiMixin, ToastMixin],
|
||||
components: {
|
||||
GenericHorizontalCard, GenericModalForm, GenericInfiniteCards, ModelMenu,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 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
|
||||
// TODO ApiGenerator doesn't capture and share error information - would be nice to share error details when available
|
||||
// or i'm capturing it incorrectly
|
||||
name: "ModelListView",
|
||||
mixins: [CardMixin, ApiMixin, ToastMixin],
|
||||
components: {
|
||||
GenericHorizontalCard,
|
||||
GenericModalForm,
|
||||
GenericInfiniteCards,
|
||||
ModelMenu,
|
||||
},
|
||||
startAction: function (e, param) {
|
||||
let source = e?.source ?? {}
|
||||
let target = e?.target ?? undefined
|
||||
this.this_item = source
|
||||
this.this_target = target
|
||||
|
||||
switch (e.action) {
|
||||
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.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;
|
||||
data() {
|
||||
return {
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
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)
|
||||
})
|
||||
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}`)
|
||||
},
|
||||
},
|
||||
getThis: function (id, callback) {
|
||||
return this.genericAPI(this.this_model, this.Actions.FETCH, {'id': id})
|
||||
},
|
||||
saveThis: function (thisItem) {
|
||||
if (!thisItem?.id) { // if there is no item id assume it's a new item
|
||||
this.genericAPI(this.this_model, this.Actions.CREATE, thisItem).then((result) => {
|
||||
// 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 = [result.data].concat(this.destroyCard(result?.data?.id, this.items_left))
|
||||
// this creates a deep copy to make sure that columns stay independent
|
||||
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)
|
||||
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")
|
||||
}
|
||||
})
|
||||
} else {
|
||||
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)
|
||||
})
|
||||
}
|
||||
this.$i18n.locale = window.CUSTOM_LOCALE
|
||||
},
|
||||
moveThis: function (source_id, target_id) {
|
||||
if (source_id === target_id) {
|
||||
this.makeToast(this.$t('Error'), this.$t('Cannot move item to itself'), '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 to do'), 'warning')
|
||||
this.clearState()
|
||||
return
|
||||
}
|
||||
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')
|
||||
})
|
||||
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) {
|
||||
let source = e?.source ?? {}
|
||||
let target = e?.target ?? undefined
|
||||
this.this_item = source
|
||||
this.this_target = target
|
||||
|
||||
if (automate) {
|
||||
let apiClient = new ApiApiFactory()
|
||||
switch (e.action) {
|
||||
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 = {
|
||||
name: `Merge ${source.name} with ${target.name}`,
|
||||
param_1: source.name,
|
||||
param_2: target.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 {
|
||||
//
|
||||
// }
|
||||
|
||||
if (this.this_model === this.Models.FOOD) {
|
||||
automation.type = 'FOOD_ALIAS'
|
||||
}
|
||||
if (this.this_model === this.Models.UNIT) {
|
||||
automation.type = 'UNIT_ALIAS'
|
||||
}
|
||||
if (this.this_model === this.Models.KEYWORD) {
|
||||
automation.type = 'KEYWORD_ALIAS'
|
||||
}
|
||||
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) {
|
||||
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()
|
||||
|
||||
},
|
||||
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)
|
||||
}
|
||||
let automation = {
|
||||
name: `Merge ${source.name} with ${target.name}`,
|
||||
param_1: source.name,
|
||||
param_2: target.name,
|
||||
}
|
||||
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
|
||||
})
|
||||
if (this.this_model === this.Models.FOOD) {
|
||||
automation.type = "FOOD_ALIAS"
|
||||
}
|
||||
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>
|
||||
|
||||
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,44 +1,45 @@
|
||||
<template>
|
||||
<div v-if="itemList">
|
||||
<span :key="k.id" v-for="k in itemList" class="pl-1">
|
||||
<b-badge pill :variant="color">{{thisLabel(k)}}</b-badge>
|
||||
</span>
|
||||
<span :key="k.id" v-for="k in itemList" class="pl-1">
|
||||
<b-badge pill :variant="color">{{ thisLabel(k) }}</b-badge>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'GenericPill',
|
||||
props: {
|
||||
item_list: {required: true, type: Array},
|
||||
label: {type: String, default: 'name'},
|
||||
color: {type: String, default: 'light'}
|
||||
},
|
||||
computed: {
|
||||
itemList: function() {
|
||||
if(Array.isArray(this.item_list)) {
|
||||
return this.item_list
|
||||
} else if (!this.item_list?.id) {
|
||||
return false
|
||||
} else {
|
||||
return [this.item_list]
|
||||
}
|
||||
name: "GenericPill",
|
||||
props: {
|
||||
item_list: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
label: { type: String, default: "name" },
|
||||
color: { type: String, default: "light" },
|
||||
},
|
||||
computed: {
|
||||
itemList: function () {
|
||||
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>
|
||||
|
@ -1,143 +1,250 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-modal :id="'modal_'+id" @hidden="cancelAction">
|
||||
<template v-slot:modal-title><h4>{{ form.title }}</h4></template>
|
||||
<div v-for="(f, i) in form.fields" v-bind:key=i>
|
||||
<p v-if="f.type=='instruction'">{{ f.label }}</p>
|
||||
<!-- this lookup is single selection -->
|
||||
<lookup-input v-if="f.type=='lookup'"
|
||||
:form="f"
|
||||
:model="listModel(f.list)"
|
||||
@change="storeValue"/> <!-- TODO add ability to create new items associated with lookup -->
|
||||
<!-- TODO: add multi-selection input list -->
|
||||
<checkbox-input v-if="f.type=='checkbox'"
|
||||
:label="f.label"
|
||||
:value="f.value"
|
||||
:field="f.field"/>
|
||||
<text-input v-if="f.type=='text'"
|
||||
:label="f.label"
|
||||
: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>
|
||||
<div>
|
||||
<b-modal :id="'modal_' + id" @hidden="cancelAction">
|
||||
<template v-slot:modal-title
|
||||
><h4>{{ form.title }}</h4></template
|
||||
>
|
||||
<div v-for="(f, i) in form.fields" v-bind:key="i">
|
||||
<p v-if="f.type == 'instruction'">{{ f.label }}</p>
|
||||
<!-- this lookup is single selection -->
|
||||
<lookup-input v-if="f.type == 'lookup'" :form="f" :model="listModel(f.list)" @change="storeValue" />
|
||||
<!-- TODO add ability to create new items associated with lookup -->
|
||||
<!-- TODO: add multi-selection input list -->
|
||||
<checkbox-input v-if="f.type == 'checkbox'" :label="f.label" :value="f.value" :field="f.field" />
|
||||
<text-input v-if="f.type == 'text'" :label="f.label" :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>
|
||||
<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>
|
||||
</template>
|
||||
</b-modal>
|
||||
</div>
|
||||
<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="primary" v-on:click="doAction">{{ form.ok_label }}</b-button>
|
||||
</template>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue'
|
||||
import {BootstrapVue} from 'bootstrap-vue'
|
||||
import {getForm} from "@/utils/utils";
|
||||
import Vue from "vue"
|
||||
import { BootstrapVue } from "bootstrap-vue"
|
||||
import { getForm } from "@/utils/utils"
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
import {Models} from "@/utils/models";
|
||||
import CheckboxInput from "@/components/Modals/CheckboxInput";
|
||||
import LookupInput from "@/components/Modals/LookupInput";
|
||||
import TextInput from "@/components/Modals/TextInput";
|
||||
import EmojiInput from "@/components/Modals/EmojiInput";
|
||||
import ChoiceInput from "@/components/Modals/ChoiceInput";
|
||||
import FileInput from "@/components/Modals/FileInput";
|
||||
import { ApiApiFactory } from "@/utils/openapi/api"
|
||||
import { ApiMixin, StandardToasts, ToastMixin } from "@/utils/utils"
|
||||
import CheckboxInput from "@/components/Modals/CheckboxInput"
|
||||
import LookupInput from "@/components/Modals/LookupInput"
|
||||
import TextInput from "@/components/Modals/TextInput"
|
||||
import EmojiInput from "@/components/Modals/EmojiInput"
|
||||
import ChoiceInput from "@/components/Modals/ChoiceInput"
|
||||
import FileInput from "@/components/Modals/FileInput"
|
||||
|
||||
export default {
|
||||
name: 'GenericModalForm',
|
||||
components: {FileInput, CheckboxInput, LookupInput, TextInput, EmojiInput, ChoiceInput},
|
||||
props: {
|
||||
model: {required: true, type: Object},
|
||||
action: {required: true, type: Object},
|
||||
item1: {
|
||||
type: Object, default() {
|
||||
return undefined
|
||||
}
|
||||
name: "GenericModalForm",
|
||||
components: { FileInput, CheckboxInput, LookupInput, TextInput, EmojiInput, ChoiceInput },
|
||||
mixins: [ApiMixin, ToastMixin],
|
||||
props: {
|
||||
model: { required: true, type: Object },
|
||||
action: { type: Object },
|
||||
item1: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
item2: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
show: { required: true, type: Boolean, default: false },
|
||||
},
|
||||
item2: {
|
||||
type: Object, default() {
|
||||
return undefined
|
||||
}
|
||||
},
|
||||
show: {required: true, type: Boolean, default: 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__
|
||||
data() {
|
||||
return {
|
||||
id: undefined,
|
||||
form_data: {},
|
||||
form: {},
|
||||
dirty: false,
|
||||
special_handling: false,
|
||||
}
|
||||
}
|
||||
return form
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.id = Math.random()
|
||||
this.$root.$on("change", this.storeValue) // boostrap modal placed at document so have to listen at root of component
|
||||
},
|
||||
computed: {
|
||||
buttonLabel() {
|
||||
return this.buttons[this.action].label
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
show: function () {
|
||||
if (this.show) {
|
||||
this.form = getForm(this.model, this.action, this.item1, this.item2)
|
||||
this.dirty = true
|
||||
this.$bvModal.show("modal_" + this.id)
|
||||
} else {
|
||||
this.$bvModal.hide("modal_" + this.id)
|
||||
this.form_data = {}
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
doAction: function () {
|
||||
this.dirty = false
|
||||
switch (this.action) {
|
||||
case this.Actions.DELETE:
|
||||
this.delete()
|
||||
break
|
||||
case this.Actions.CREATE:
|
||||
this.save()
|
||||
break
|
||||
case this.Actions.UPDATE:
|
||||
this.form_data.id = this.item1.id
|
||||
this.save()
|
||||
break
|
||||
case this.Actions.MERGE:
|
||||
this.merge(this.item1, this.form_data.target.id, this.item1?.automate ?? false)
|
||||
break
|
||||
case this.Actions.MOVE:
|
||||
this.move(this.item1.id, this.form_data.target.id)
|
||||
break
|
||||
}
|
||||
},
|
||||
cancelAction: function () {
|
||||
if (this.dirty) {
|
||||
this.dirty = false
|
||||
this.$emit("finish-action", "cancel")
|
||||
}
|
||||
},
|
||||
storeValue: function (field, value) {
|
||||
this.form_data[field] = value
|
||||
},
|
||||
listModel: function (m) {
|
||||
if (m === "self") {
|
||||
return this.model
|
||||
} else {
|
||||
return this.Models[m]
|
||||
}
|
||||
},
|
||||
detectOverride: function (form) {
|
||||
for (const [k, v] of Object.entries(form)) {
|
||||
if (form[k].__override__) {
|
||||
form[k] = form[k].__override__
|
||||
}
|
||||
}
|
||||
return form
|
||||
},
|
||||
delete: function () {
|
||||
this.genericAPI(this.model, this.Actions.DELETE, { id: this.item1.id })
|
||||
.then((result) => {
|
||||
this.$emit("finish-action")
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_DELETE)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_DELETE)
|
||||
this.$emit("finish-action", "cancel")
|
||||
})
|
||||
},
|
||||
save: function () {
|
||||
if (!this.item1?.id) {
|
||||
// if there is no item id assume it's a new item
|
||||
this.genericAPI(this.model, this.Actions.CREATE, this.form_data)
|
||||
.then((result) => {
|
||||
this.$emit("finish-action", { item: result.data })
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
|
||||
this.$emit("finish-action", "cancel")
|
||||
})
|
||||
} else {
|
||||
this.genericAPI(this.model, this.Actions.UPDATE, this.form_data)
|
||||
.then((result) => {
|
||||
this.$emit("finish-action")
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err, err.response)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
|
||||
this.$emit("finish-action", "cancel")
|
||||
})
|
||||
}
|
||||
},
|
||||
move: function () {
|
||||
if (this.item1.id === this.form_data.target.id) {
|
||||
this.makeToast(this.$t("Error"), this.$t("err_move_self"), "danger")
|
||||
this.$emit("finish-action", "cancel")
|
||||
return
|
||||
}
|
||||
if (this.form_data.target.id === undefined || this.item1?.parent == this.form_data.target.id) {
|
||||
this.makeToast(this.$t("Warning"), this.$t("nothing"), "warning")
|
||||
this.$emit("finish-action", "cancel")
|
||||
return
|
||||
}
|
||||
this.genericAPI(this.model, this.Actions.MOVE, { source: this.item1.id, target: this.form_data.target.id })
|
||||
.then((result) => {
|
||||
this.$emit("finish-action", { target: this.form_data.target.id })
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_MOVE)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_MOVE)
|
||||
this.$emit("finish-action", "cancel")
|
||||
})
|
||||
},
|
||||
merge: function () {
|
||||
if (this.item1.id === this.form_data.target.id) {
|
||||
this.makeToast(this.$t("Error"), this.$t("err_merge_self"), "danger")
|
||||
this.$emit("finish-action", "cancel")
|
||||
return
|
||||
}
|
||||
if (!this.item1.id || !this.form_data.target.id) {
|
||||
this.makeToast(this.$t("Warning"), this.$t("nothing"), "warning")
|
||||
this.$emit("finish-action", "cancel")
|
||||
return
|
||||
}
|
||||
this.genericAPI(this.model, this.Actions.MERGE, {
|
||||
source: this.item1.id,
|
||||
target: this.form_data.target.id,
|
||||
})
|
||||
.then((result) => {
|
||||
this.$emit("finish-action", { target: this.form_data.target.id })
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_MERGE)
|
||||
})
|
||||
.catch((err) => {
|
||||
//TODO error checking not working with OpenAPI methods
|
||||
console.log("Error", err)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_MERGE)
|
||||
this.$emit("finish-action", "cancel")
|
||||
})
|
||||
|
||||
if (this.item1.automate) {
|
||||
let apiClient = new ApiApiFactory()
|
||||
let automation = {
|
||||
name: `Merge ${this.item1.name} with ${this.form_data.target.name}`,
|
||||
param_1: this.item1.name,
|
||||
param_2: this.form_data.target.name,
|
||||
}
|
||||
|
||||
if (this.model === this.Models.FOOD) {
|
||||
automation.type = "FOOD_ALIAS"
|
||||
}
|
||||
if (this.model === this.Models.UNIT) {
|
||||
automation.type = "UNIT_ALIAS"
|
||||
}
|
||||
if (this.model === this.Models.KEYWORD) {
|
||||
automation.type = "KEYWORD_ALIAS"
|
||||
}
|
||||
|
||||
apiClient.createAutomation(automation)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
@ -1,157 +1,171 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-form-group
|
||||
v-bind:label="form.label"
|
||||
class="mb-3">
|
||||
<generic-multiselect
|
||||
@change="new_value=$event.val"
|
||||
@remove="new_value=undefined"
|
||||
:initial_selection="initialSelection"
|
||||
:model="model"
|
||||
:multiple="useMultiple"
|
||||
:sticky_options="sticky_options"
|
||||
:allow_create="create_new"
|
||||
:create_placeholder="createPlaceholder"
|
||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||
:placeholder="modelName"
|
||||
@new="addNew">
|
||||
</generic-multiselect>
|
||||
<b-form-group class="mb-3">
|
||||
<template #label v-if="show_label">
|
||||
{{ form.label }}
|
||||
</template>
|
||||
<generic-multiselect
|
||||
@change="new_value = $event.val"
|
||||
@remove="new_value = undefined"
|
||||
:initial_selection="initialSelection"
|
||||
:model="model"
|
||||
:multiple="useMultiple"
|
||||
:sticky_options="sticky_options"
|
||||
:allow_create="form.allow_create"
|
||||
:create_placeholder="createPlaceholder"
|
||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||
:placeholder="modelName"
|
||||
@new="addNew"
|
||||
>
|
||||
</generic-multiselect>
|
||||
</b-form-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GenericMultiselect from "@/components/GenericMultiselect";
|
||||
import {StandardToasts, ApiMixin} from "@/utils/utils";
|
||||
import GenericMultiselect from "@/components/GenericMultiselect"
|
||||
import { StandardToasts, ApiMixin } from "@/utils/utils"
|
||||
|
||||
export default {
|
||||
name: 'LookupInput',
|
||||
components: {GenericMultiselect},
|
||||
mixins: [ApiMixin],
|
||||
props: {
|
||||
form: {type: Object, default () {return undefined}},
|
||||
model: {type: Object, default () {return undefined}},
|
||||
|
||||
// TODO: include create_new and create_text props and associated functionality to create objects for drop down
|
||||
// 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
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
new_value: undefined,
|
||||
field: undefined,
|
||||
label: undefined,
|
||||
sticky_options: undefined,
|
||||
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')
|
||||
name: "LookupInput",
|
||||
components: { GenericMultiselect },
|
||||
mixins: [ApiMixin],
|
||||
props: {
|
||||
form: {
|
||||
type: Object,
|
||||
default() {
|
||||
return undefined
|
||||
},
|
||||
},
|
||||
model: {
|
||||
type: Object,
|
||||
default() {
|
||||
return undefined
|
||||
},
|
||||
},
|
||||
show_label: { type: Boolean, default: true },
|
||||
},
|
||||
useMultiple() {
|
||||
return this.form?.multiple || this.form?.ordered || false
|
||||
},
|
||||
initialSelection() {
|
||||
let this_value = this.form.value
|
||||
let arrayValues = undefined
|
||||
// multiselect is expect to get an array of objects - make sure it gets one
|
||||
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
|
||||
}
|
||||
data() {
|
||||
return {
|
||||
new_value: undefined,
|
||||
field: undefined,
|
||||
label: undefined,
|
||||
sticky_options: undefined,
|
||||
first_run: true,
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
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() {
|
||||
return this.form?.multiple || this.form?.ordered || false
|
||||
},
|
||||
initialSelection() {
|
||||
let this_value = this.new_value
|
||||
let arrayValues = undefined
|
||||
// multiselect is expect to get an array of objects - make sure it gets one
|
||||
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: {
|
||||
"form.value": function (newVal, oldVal) {
|
||||
this.new_value = newVal
|
||||
},
|
||||
new_value: function () {
|
||||
let x = this?.new_value
|
||||
// pass the unflattened attributes that can be restored when ready to save/update
|
||||
if (this.form?.ordered) {
|
||||
x["__override__"] = this.unflattenItem(this?.new_value)
|
||||
}
|
||||
this.$root.$emit("change", this.form.field, x)
|
||||
this.$emit("change", x)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
addNew: function (e) {
|
||||
// if create a new item requires more than 1 parameter or the field 'name' is insufficient this will need reworked
|
||||
// in a perfect world this would trigger a new modal and allow editing all fields
|
||||
this.genericAPI(this.model, this.Actions.CREATE, { name: e })
|
||||
.then((result) => {
|
||||
this.new_value = result.data
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
|
||||
})
|
||||
},
|
||||
// ordered lookups have nested attributes that need flattened attributes to drive lookup
|
||||
flattenItems: function (itemlist) {
|
||||
let flat_items = []
|
||||
let item = undefined
|
||||
let label = this.form.list_label.split("::")
|
||||
itemlist.forEach((x) => {
|
||||
item = {}
|
||||
for (const [k, v] of Object.entries(x)) {
|
||||
if (k == label[0]) {
|
||||
item["id"] = v.id
|
||||
item[label[1]] = v[label[1]]
|
||||
} else {
|
||||
item[this.form.field + "__" + k] = v
|
||||
}
|
||||
}
|
||||
flat_items.push(item)
|
||||
})
|
||||
this.first_run = false
|
||||
return flat_items
|
||||
},
|
||||
unflattenItem: function (itemList) {
|
||||
let unflat_items = []
|
||||
let item = undefined
|
||||
let this_label = undefined
|
||||
let label = this.form.list_label.split("::")
|
||||
let order = 0
|
||||
itemList.forEach((x) => {
|
||||
item = {}
|
||||
item[label[0]] = {}
|
||||
for (const [k, v] of Object.entries(x)) {
|
||||
switch (k) {
|
||||
case "id":
|
||||
item[label[0]]["id"] = v
|
||||
break
|
||||
case label[1]:
|
||||
item[label[0]][label[1]] = v
|
||||
break
|
||||
default:
|
||||
this_label = k.replace(this.form.field + "__", "")
|
||||
}
|
||||
}
|
||||
item["order"] = order
|
||||
order++
|
||||
unflat_items.push(item)
|
||||
})
|
||||
return unflat_items
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
@ -4,10 +4,14 @@
|
||||
"err_creating_resource": "There was an error creating a resource!",
|
||||
"err_updating_resource": "There was an error updating 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_creating_resource": "Successfully created a resource!",
|
||||
"success_updating_resource": "Successfully updated 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.",
|
||||
"step_time_minutes": "Step time in minutes",
|
||||
"confirm_delete": "Are you sure you want to delete this {object}?",
|
||||
@ -207,5 +211,8 @@
|
||||
"New_Cookbook": "New cookbook",
|
||||
"Hide_Keyword": "Hide keywords",
|
||||
"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"
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,15 +1,26 @@
|
||||
/*
|
||||
* Utility functions to call bootstrap toasts
|
||||
* */
|
||||
import {BToast} from 'bootstrap-vue'
|
||||
import i18n from "@/i18n";
|
||||
* Utility functions to call bootstrap toasts
|
||||
* */
|
||||
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 = {
|
||||
methods: {
|
||||
makeToast: function (title, message, variant = null) {
|
||||
return makeToast(title, message, variant)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export function makeToast(title, message, variant = null) {
|
||||
@ -17,57 +28,71 @@ export function makeToast(title, message, variant = null) {
|
||||
toaster.$bvToast.toast(message, {
|
||||
title: title,
|
||||
variant: variant,
|
||||
toaster: 'b-toaster-bottom-right',
|
||||
solid: true
|
||||
toaster: "b-toaster-bottom-right",
|
||||
solid: true,
|
||||
})
|
||||
}
|
||||
|
||||
export class StandardToasts {
|
||||
static SUCCESS_CREATE = 'SUCCESS_CREATE'
|
||||
static SUCCESS_FETCH = 'SUCCESS_FETCH'
|
||||
static SUCCESS_UPDATE = 'SUCCESS_UPDATE'
|
||||
static SUCCESS_DELETE = 'SUCCESS_DELETE'
|
||||
static SUCCESS_CREATE = "SUCCESS_CREATE"
|
||||
static SUCCESS_FETCH = "SUCCESS_FETCH"
|
||||
static SUCCESS_UPDATE = "SUCCESS_UPDATE"
|
||||
static SUCCESS_DELETE = "SUCCESS_DELETE"
|
||||
static SUCCESS_MOVE = "SUCCESS_MOVE"
|
||||
static SUCCESS_MERGE = "SUCCESS_MERGE"
|
||||
|
||||
static FAIL_CREATE = 'FAIL_CREATE'
|
||||
static FAIL_FETCH = 'FAIL_FETCH'
|
||||
static FAIL_UPDATE = 'FAIL_UPDATE'
|
||||
static FAIL_DELETE = 'FAIL_DELETE'
|
||||
static FAIL_CREATE = "FAIL_CREATE"
|
||||
static FAIL_FETCH = "FAIL_FETCH"
|
||||
static FAIL_UPDATE = "FAIL_UPDATE"
|
||||
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) {
|
||||
case StandardToasts.SUCCESS_CREATE:
|
||||
makeToast(i18n.tc('Success'), i18n.tc('success_creating_resource'), 'success')
|
||||
break;
|
||||
makeToast(i18n.tc("Success"), i18n.tc("success_creating_resource"), "success")
|
||||
break
|
||||
case StandardToasts.SUCCESS_FETCH:
|
||||
makeToast(i18n.tc('Success'), i18n.tc('success_fetching_resource'), 'success')
|
||||
break;
|
||||
makeToast(i18n.tc("Success"), i18n.tc("success_fetching_resource"), "success")
|
||||
break
|
||||
case StandardToasts.SUCCESS_UPDATE:
|
||||
makeToast(i18n.tc('Success'), i18n.tc('success_updating_resource'), 'success')
|
||||
break;
|
||||
makeToast(i18n.tc("Success"), i18n.tc("success_updating_resource"), "success")
|
||||
break
|
||||
case StandardToasts.SUCCESS_DELETE:
|
||||
makeToast(i18n.tc('Success'), i18n.tc('success_deleting_resource'), 'success')
|
||||
break;
|
||||
makeToast(i18n.tc("Success"), i18n.tc("success_deleting_resource"), "success")
|
||||
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:
|
||||
makeToast(i18n.tc('Failure'), i18n.tc('err_creating_resource'), 'danger')
|
||||
break;
|
||||
makeToast(i18n.tc("Failure"), i18n.tc("err_creating_resource"), "danger")
|
||||
break
|
||||
case StandardToasts.FAIL_FETCH:
|
||||
makeToast(i18n.tc('Failure'), i18n.tc('err_fetching_resource'), 'danger')
|
||||
break;
|
||||
makeToast(i18n.tc("Failure"), i18n.tc("err_fetching_resource"), "danger")
|
||||
break
|
||||
case StandardToasts.FAIL_UPDATE:
|
||||
makeToast(i18n.tc('Failure'), i18n.tc('err_updating_resource'), 'danger')
|
||||
break;
|
||||
makeToast(i18n.tc("Failure"), i18n.tc("err_updating_resource"), "danger")
|
||||
break
|
||||
case StandardToasts.FAIL_DELETE:
|
||||
makeToast(i18n.tc('Failure'), i18n.tc('err_deleting_resource'), 'danger')
|
||||
break;
|
||||
|
||||
makeToast(i18n.tc("Failure"), i18n.tc("err_deleting_resource"), "danger")
|
||||
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 = {
|
||||
methods: {
|
||||
@ -77,8 +102,8 @@ export const GettextMixin = {
|
||||
*/
|
||||
_: function (param) {
|
||||
return 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
|
||||
export const ResolveUrlMixin = {
|
||||
@ -99,50 +124,48 @@ export const ResolveUrlMixin = {
|
||||
*/
|
||||
resolveDjangoUrl: function (url, params = null) {
|
||||
return resolveDjangoUrl(url, params)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export function resolveDjangoUrl(url, params = null) {
|
||||
if (params == null) {
|
||||
return window.Urls[url]()
|
||||
} else if (typeof(params) != "object") {
|
||||
} else if (typeof params != "object") {
|
||||
return window.Urls[url](params)
|
||||
} else if (typeof(params) == "object") {
|
||||
} else if (typeof params == "object") {
|
||||
if (params.length === 1) {
|
||||
return window.Urls[url](params)
|
||||
} 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) {
|
||||
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) {
|
||||
if(window.USER_PREF === undefined) {
|
||||
return undefined;
|
||||
if (window.USER_PREF === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return window.USER_PREF[pref]
|
||||
}
|
||||
|
||||
import {frac} from "@/utils/fractions";
|
||||
|
||||
export function calculateAmount(amount, factor) {
|
||||
if (getUserPreference('use_fractions')) {
|
||||
let return_string = ''
|
||||
let fraction = frac((amount * factor), 10, true)
|
||||
if (getUserPreference("use_fractions")) {
|
||||
let return_string = ""
|
||||
let fraction = frac(amount * factor, 10, true)
|
||||
|
||||
if (fraction[0] > 0) {
|
||||
return_string += fraction[0]
|
||||
}
|
||||
|
||||
if (fraction[1] > 0) {
|
||||
return_string += ` <sup>${(fraction[1])}</sup>⁄<sub>${(fraction[2])}</sub>`
|
||||
return_string += ` <sup>${fraction[1]}</sup>⁄<sub>${fraction[2]}</sub>`
|
||||
}
|
||||
|
||||
return return_string
|
||||
@ -152,23 +175,23 @@ export function calculateAmount(amount, factor) {
|
||||
}
|
||||
|
||||
export function roundDecimals(num) {
|
||||
let decimals = ((getUserPreference('user_fractions')) ? getUserPreference('user_fractions') : 2);
|
||||
return +(Math.round(num + `e+${decimals}`) + `e-${decimals}`);
|
||||
let decimals = getUserPreference("user_fractions") ? getUserPreference("user_fractions") : 2
|
||||
return +(Math.round(num + `e+${decimals}`) + `e-${decimals}`)
|
||||
}
|
||||
|
||||
const KILOJOULES_PER_CALORIE = 4.18
|
||||
|
||||
export function calculateEnergy(amount, factor) {
|
||||
if (getUserPreference('use_kj')) {
|
||||
if (getUserPreference("use_kj")) {
|
||||
let joules = amount * KILOJOULES_PER_CALORIE
|
||||
return calculateAmount(joules, factor) + ' kJ'
|
||||
return calculateAmount(joules, factor) + " kJ"
|
||||
} else {
|
||||
return calculateAmount(amount, factor) + ' kcal'
|
||||
return calculateAmount(amount, factor) + " kcal"
|
||||
}
|
||||
}
|
||||
|
||||
export function convertEnergyToCalories(amount) {
|
||||
if (getUserPreference('use_kj')) {
|
||||
if (getUserPreference("use_kj")) {
|
||||
return amount / KILOJOULES_PER_CALORIE
|
||||
} else {
|
||||
return amount
|
||||
@ -176,33 +199,25 @@ export function convertEnergyToCalories(amount) {
|
||||
}
|
||||
|
||||
export function energyHeading() {
|
||||
if (getUserPreference('use_kj')) {
|
||||
return 'Energy'
|
||||
if (getUserPreference("use_kj")) {
|
||||
return "Energy"
|
||||
} else {
|
||||
return 'Calories'
|
||||
return "Calories"
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Utility functions to use OpenAPIs generically
|
||||
* */
|
||||
import {ApiApiFactory} from "@/utils/openapi/api.ts";
|
||||
|
||||
import axios from "axios";
|
||||
axios.defaults.xsrfCookieName = 'csrftoken'
|
||||
axios.defaults.xsrfCookieName = "csrftoken"
|
||||
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
|
||||
import { Actions, Models } from './models';
|
||||
import {RequestArgs} from "@/utils/openapi/base";
|
||||
|
||||
export const ApiMixin = {
|
||||
data() {
|
||||
return {
|
||||
Models: Models,
|
||||
Actions: Actions
|
||||
Actions: Actions,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
genericAPI: function(model, action, options) {
|
||||
genericAPI: function (model, action, options) {
|
||||
let setup = getConfig(model, action)
|
||||
if (setup?.config?.function) {
|
||||
return specialCases[setup.config.function](action, options, setup)
|
||||
@ -212,10 +227,10 @@ export const ApiMixin = {
|
||||
let apiClient = new ApiApiFactory()
|
||||
return apiClient[func](...parameters)
|
||||
},
|
||||
genericGetAPI: function(url, options) {
|
||||
return axios.get(this.resolveDjangoUrl(url), {'params':options, 'emulateJSON': true})
|
||||
}
|
||||
}
|
||||
genericGetAPI: function (url, options) {
|
||||
return axios.get(this.resolveDjangoUrl(url), { params: options, emulateJSON: true })
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// /*
|
||||
@ -223,37 +238,37 @@ export const ApiMixin = {
|
||||
// * */
|
||||
function formatParam(config, value, options) {
|
||||
if (config) {
|
||||
for (const [k, v] of Object.entries(config)) {
|
||||
switch(k) {
|
||||
case 'type':
|
||||
switch(v) {
|
||||
case 'string':
|
||||
for (const [k, v] of Object.entries(config)) {
|
||||
switch (k) {
|
||||
case "type":
|
||||
switch (v) {
|
||||
case "string":
|
||||
if (Array.isArray(value)) {
|
||||
let tmpValue = []
|
||||
value.forEach(x => tmpValue.push(String(x)))
|
||||
value.forEach((x) => tmpValue.push(String(x)))
|
||||
value = tmpValue
|
||||
} else if (value !== undefined) {
|
||||
value = String(value)
|
||||
}
|
||||
break;
|
||||
case 'integer':
|
||||
break
|
||||
case "integer":
|
||||
if (Array.isArray(value)) {
|
||||
let tmpValue = []
|
||||
value.forEach(x => tmpValue.push(parseInt(x)))
|
||||
value.forEach((x) => tmpValue.push(parseInt(x)))
|
||||
value = tmpValue
|
||||
} else if (value !== undefined) {
|
||||
value = parseInt(value)
|
||||
}
|
||||
break;
|
||||
break
|
||||
}
|
||||
break;
|
||||
case 'function':
|
||||
break
|
||||
case "function":
|
||||
// needs wrapped in a promise and wait for the called function to complete before moving on
|
||||
specialCases[v](value, options)
|
||||
break;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
function buildParams(options, setup) {
|
||||
@ -280,60 +295,56 @@ function buildParams(options, setup) {
|
||||
this_value = getDefault(config?.[item], options)
|
||||
}
|
||||
parameters.push(this_value)
|
||||
});
|
||||
})
|
||||
return parameters
|
||||
}
|
||||
function getDefault(config, options) {
|
||||
let value = undefined
|
||||
value = config?.default ?? undefined
|
||||
if (typeof(value) === 'object') {
|
||||
if (typeof value === "object") {
|
||||
let condition = false
|
||||
switch(value.function) {
|
||||
switch (value.function) {
|
||||
// CONDITIONAL case requires 4 keys:
|
||||
// - check: which other OPTIONS key to check against
|
||||
// - operator: what type of operation to perform
|
||||
// - true: what value to assign when true
|
||||
// - false: what value to assign when false
|
||||
case 'CONDITIONAL':
|
||||
switch(value.operator) {
|
||||
case 'not_exist':
|
||||
condition = (
|
||||
(!options?.[value.check] ?? undefined)
|
||||
|| options?.[value.check]?.length == 0
|
||||
)
|
||||
case "CONDITIONAL":
|
||||
switch (value.operator) {
|
||||
case "not_exist":
|
||||
condition = (!options?.[value.check] ?? undefined) || options?.[value.check]?.length == 0
|
||||
if (condition) {
|
||||
value = value.true
|
||||
} else {
|
||||
value = value.false
|
||||
}
|
||||
break;
|
||||
break
|
||||
}
|
||||
break;
|
||||
break
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
export function getConfig(model, action) {
|
||||
|
||||
let f = action.function
|
||||
// if not defined partialUpdate will use params from create
|
||||
if (f === 'partialUpdate' && !model?.[f]?.params) {
|
||||
model[f] = {'params': [...['id'], ...model.create.params]}
|
||||
if (f === "partialUpdate" && !model?.[f]?.params) {
|
||||
model[f] = { params: [...["id"], ...model.create.params] }
|
||||
}
|
||||
|
||||
|
||||
let config = {
|
||||
'name': model.name,
|
||||
'apiName': model.apiName,
|
||||
name: model.name,
|
||||
apiName: model.apiName,
|
||||
}
|
||||
// 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
|
||||
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
|
||||
if (f === 'partialUpdate' && Object.keys(config.config).length === 0) {
|
||||
config.config = {...model.model_type?.create?.config, ...model?.create?.config}
|
||||
if (f === "partialUpdate" && Object.keys(config.config).length === 0) {
|
||||
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
|
||||
}
|
||||
|
||||
@ -342,181 +353,175 @@ export function getConfig(model, action) {
|
||||
// * */
|
||||
export function getForm(model, action, item1, item2) {
|
||||
let f = action.function
|
||||
let config = {...action?.form, ...model.model_type?.[f]?.form, ...model?.[f]?.form}
|
||||
// if not defined partialUpdate will use form from create
|
||||
if (f === 'partialUpdate' && Object.keys(config).length == 0) {
|
||||
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}
|
||||
let config = { ...action?.form, ...model.model_type?.[f]?.form, ...model?.[f]?.form }
|
||||
// if not defined partialUpdate will use form from create
|
||||
if (f === "partialUpdate" && Object.keys(config).length == 0) {
|
||||
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 }
|
||||
}
|
||||
let form = {'fields': []}
|
||||
let value = ''
|
||||
let form = { fields: [] }
|
||||
let value = ""
|
||||
for (const [k, v] of Object.entries(config)) {
|
||||
if (v?.function){
|
||||
switch(v.function) {
|
||||
case 'translate':
|
||||
if (v?.function) {
|
||||
switch (v.function) {
|
||||
case "translate":
|
||||
value = formTranslate(v, model, item1, item2)
|
||||
}
|
||||
} else {
|
||||
value = v
|
||||
}
|
||||
if (value?.form_field) {
|
||||
value['value'] = item1?.[value?.field] ?? undefined
|
||||
form.fields.push(
|
||||
{
|
||||
...value,
|
||||
...{
|
||||
'label': formTranslate(value?.label, model, item1, item2),
|
||||
'placeholder': formTranslate(value?.placeholder, model, item1, item2)
|
||||
}
|
||||
}
|
||||
)
|
||||
value["value"] = item1?.[value?.field] ?? undefined
|
||||
form.fields.push({
|
||||
...value,
|
||||
...{
|
||||
label: formTranslate(value?.label, model, item1, item2),
|
||||
placeholder: formTranslate(value?.placeholder, model, item1, item2),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
form[k] = value
|
||||
}
|
||||
}
|
||||
return form
|
||||
|
||||
}
|
||||
function formTranslate(translate, model, item1, item2) {
|
||||
if (typeof(translate) !== 'object') {return translate}
|
||||
if (typeof translate !== "object") {
|
||||
return translate
|
||||
}
|
||||
let phrase = translate.phrase
|
||||
let options = {}
|
||||
let obj = undefined
|
||||
translate?.params.forEach(function (x, index) {
|
||||
switch(x.from){
|
||||
case 'item1':
|
||||
switch (x.from) {
|
||||
case "item1":
|
||||
obj = item1
|
||||
break;
|
||||
case 'item2':
|
||||
break
|
||||
case "item2":
|
||||
obj = item2
|
||||
break;
|
||||
case 'model':
|
||||
break
|
||||
case "model":
|
||||
obj = model
|
||||
}
|
||||
options[x.token] = obj[x.attribute]
|
||||
})
|
||||
return i18n.t(phrase, options)
|
||||
|
||||
}
|
||||
|
||||
// /*
|
||||
// * Utility functions to use manipulate nested components
|
||||
// * */
|
||||
import Vue from 'vue'
|
||||
export const CardMixin = {
|
||||
methods: {
|
||||
findCard: function(id, card_list){
|
||||
findCard: function (id, card_list) {
|
||||
let card_length = card_list?.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) {
|
||||
return cards[0]
|
||||
return cards[0]
|
||||
} else if (cards.length == 0) {
|
||||
for (const c of card_list.filter(x => x.show_children == true)) {
|
||||
cards = this.findCard(id, c.children)
|
||||
if (cards) {
|
||||
return cards
|
||||
for (const c of card_list.filter((x) => x.show_children == true)) {
|
||||
cards = this.findCard(id, c.children)
|
||||
if (cards) {
|
||||
return cards
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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 p_id = card?.parent ?? undefined
|
||||
|
||||
|
||||
if (p_id) {
|
||||
let parent = this.findCard(p_id, card_list)
|
||||
if (parent){
|
||||
Vue.set(parent, 'numchild', parent.numchild - 1)
|
||||
if (parent) {
|
||||
Vue.set(parent, "numchild", parent.numchild - 1)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
return card_list.filter(x => x.id != id)
|
||||
},
|
||||
refreshCard: function(obj, card_list){
|
||||
return card_list.filter((x) => x.id != id)
|
||||
},
|
||||
refreshCard: function (obj, card_list) {
|
||||
let target = {}
|
||||
let idx = undefined
|
||||
target = this.findCard(obj.id, card_list)
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
if (target?.parent) {
|
||||
let parent = this.findCard(target.parent, card_list)
|
||||
if (parent) {
|
||||
if (parent.show_children){
|
||||
idx = parent.children.indexOf(parent.children.find(x => x.id === target.id))
|
||||
if (parent.show_children) {
|
||||
idx = parent.children.indexOf(parent.children.find((x) => x.id === target.id))
|
||||
Vue.set(parent.children, idx, obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
const specialCases = {
|
||||
// the supermarket API requires chaining promises together, instead of trying to make
|
||||
// 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 GenericAPI = ApiMixin.methods.genericAPI
|
||||
let params = []
|
||||
if (action.function === 'partialUpdate') {
|
||||
if (action.function === "partialUpdate") {
|
||||
API = GenericAPI
|
||||
params = [Models.SUPERMARKET, Actions.FETCH, {'id': options.id}]
|
||||
|
||||
} else if (action.function === 'create') {
|
||||
params = [Models.SUPERMARKET, Actions.FETCH, { id: options.id }]
|
||||
} else if (action.function === "create") {
|
||||
API = new ApiApiFactory()[setup.function]
|
||||
params = buildParams(options, setup)
|
||||
}
|
||||
|
||||
return API(...params).then((result) => {
|
||||
// either get the supermarket or create the supermarket (but without the category relations)
|
||||
return result.data
|
||||
}).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})
|
||||
return API(...params)
|
||||
.then((result) => {
|
||||
// either get the supermarket or create the supermarket (but without the category relations)
|
||||
return result.data
|
||||
})
|
||||
})
|
||||
}
|
||||
.then((result) => {
|
||||
// delete, update or change all of the category/relations
|
||||
let id = result.id
|
||||
let existing_categories = result.category_to_supermarket
|
||||
let updated_categories = options.category_to_supermarket
|
||||
|
||||
let promises = []
|
||||
// if the 'category.name' key does not exist on the updated_categories, the categories were not updated
|
||||
if (updated_categories?.[0]?.category?.name) {
|
||||
// list of category relationship ids that are not part of the updated supermarket
|
||||
let removed_categories = existing_categories.filter((x) => !updated_categories.map((x) => x.category.id).includes(x.category.id))
|
||||
let added_categories = updated_categories.filter((x) => !existing_categories.map((x) => x.category.id).includes(x.category.id))
|
||||
let changed_categories = updated_categories.filter((x) => existing_categories.map((x) => x.category.id).includes(x.category.id))
|
||||
|
||||
removed_categories.forEach((x) => {
|
||||
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.DELETE, { id: x.id }))
|
||||
})
|
||||
let item = { supermarket: id }
|
||||
added_categories.forEach((x) => {
|
||||
item.order = x.order
|
||||
item.category = { id: x.category.id, name: x.category.name }
|
||||
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.CREATE, item))
|
||||
})
|
||||
changed_categories.forEach((x) => {
|
||||
item.id = x?.id ?? existing_categories.find((y) => y.category.id === x.category.id).id
|
||||
item.order = x.order
|
||||
item.category = { id: x.category.id, name: x.category.name }
|
||||
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.UPDATE, item))
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
// finally get and return the Supermarket which everything downstream is expecting
|
||||
return GenericAPI(Models.SUPERMARKET, Actions.FETCH, { id: id })
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user