Merge branch 'develop' of https://github.com/vabene1111/recipes into develop

# Conflicts:
#	cookbook/templates/base.html
This commit is contained in:
Kaibu
2021-09-13 22:15:24 +02:00
25 changed files with 523 additions and 324 deletions

View File

@@ -276,8 +276,9 @@ class UnitSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerializer): class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
def create(self, validated_data): def create(self, validated_data):
obj, created = SupermarketCategory.objects.get_or_create(name=validated_data['name'], validated_data['name'] = validated_data['name'].strip()
space=self.context['request'].space) validated_data['space'] = self.context['request'].space
obj, created = SupermarketCategory.objects.get_or_create(**validated_data)
return obj return obj
def update(self, instance, validated_data): def update(self, instance, validated_data):
@@ -285,7 +286,7 @@ class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerial
class Meta: class Meta:
model = SupermarketCategory model = SupermarketCategory
fields = ('id', 'name') fields = ('id', 'name', 'description')
class SupermarketCategoryRelationSerializer(WritableNestedModelSerializer): class SupermarketCategoryRelationSerializer(WritableNestedModelSerializer):
@@ -301,7 +302,7 @@ class SupermarketSerializer(UniqueFieldsMixin, SpacedModelSerializer):
class Meta: class Meta:
model = Supermarket model = Supermarket
fields = ('id', 'name', 'category_to_supermarket') fields = ('id', 'name', 'description', 'category_to_supermarket')
class RecipeSimpleSerializer(serializers.ModelSerializer): class RecipeSimpleSerializer(serializers.ModelSerializer):

View File

@@ -1 +1 @@
.flip-enter-active[data-v-8633bda0]{-webkit-animation-name:bounceUp-data-v-8633bda0;animation-name:bounceUp-data-v-8633bda0;-webkit-animation-duration:.5s;animation-duration:.5s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-timing-function:linear;animation-timing-function:linear;animation-iteration-count:infinite;-webkit-animation-iteration-count:infinite}.bounceleft[data-v-8633bda0]{-webkit-animation-name:bounceLeft-data-v-8633bda0;animation-name:bounceLeft-data-v-8633bda0;-webkit-animation-duration:.5s;animation-duration:.5s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-timing-function:linear;animation-timing-function:linear;animation-iteration-count:1;-webkit-animation-iteration-count:1}.bounceright[data-v-8633bda0]{-webkit-animation-name:bounceRight-data-v-8633bda0;animation-name:bounceRight-data-v-8633bda0;-webkit-animation-duration:.5s;animation-duration:.5s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-timing-function:linear;animation-timing-function:linear;animation-iteration-count:1;-webkit-animation-iteration-count:1}@-webkit-keyframes bounceUp-data-v-8633bda0{0%,to{-webkit-transform:translateY(0)}50%{-webkit-transform:translateY(-7px)}}@keyframes bounceUp-data-v-8633bda0{0%,to{transform:translateY(0)}50%{transform:translateY(-7px)}}@-webkit-keyframes bounceLeft-data-v-8633bda0{0%,to{-webkit-transform:translateY(0)}50%{-webkit-transform:translateX(-10px)}}@keyframes bounceLeft-data-v-8633bda0{0%,to{transform:translateY(0)}50%{transform:translateX(-10px)}}@-webkit-keyframes bounceRight-data-v-8633bda0{0%,to{-webkit-transform:translateY(0)}50%{-webkit-transform:translateX(10px)}}@keyframes bounceRight-data-v-8633bda0{0%,to{transform:translateY(0)}50%{transform:translateX(10px)}}.slide-fade-enter-active{transition:all .6s ease}.slide-fade-enter,.slide-fade-leave-to{transform:translateX(10px);opacity:0} .touchable[data-v-18b1d8a0]{padding-right:2em;padding-left:2em;margin-right:-2em;margin-left:-2em}.flip-enter-active[data-v-8633bda0]{-webkit-animation-name:bounceUp-data-v-8633bda0;animation-name:bounceUp-data-v-8633bda0;-webkit-animation-duration:.5s;animation-duration:.5s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-timing-function:linear;animation-timing-function:linear;animation-iteration-count:infinite;-webkit-animation-iteration-count:infinite}.bounceleft[data-v-8633bda0]{-webkit-animation-name:bounceLeft-data-v-8633bda0;animation-name:bounceLeft-data-v-8633bda0;-webkit-animation-duration:.5s;animation-duration:.5s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-timing-function:linear;animation-timing-function:linear;animation-iteration-count:1;-webkit-animation-iteration-count:1}.bounceright[data-v-8633bda0]{-webkit-animation-name:bounceRight-data-v-8633bda0;animation-name:bounceRight-data-v-8633bda0;-webkit-animation-duration:.5s;animation-duration:.5s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-timing-function:linear;animation-timing-function:linear;animation-iteration-count:1;-webkit-animation-iteration-count:1}@-webkit-keyframes bounceUp-data-v-8633bda0{0%,to{-webkit-transform:translateY(0)}50%{-webkit-transform:translateY(-7px)}}@keyframes bounceUp-data-v-8633bda0{0%,to{transform:translateY(0)}50%{transform:translateY(-7px)}}@-webkit-keyframes bounceLeft-data-v-8633bda0{0%,to{-webkit-transform:translateY(0)}50%{-webkit-transform:translateX(-10px)}}@keyframes bounceLeft-data-v-8633bda0{0%,to{transform:translateY(0)}50%{transform:translateX(-10px)}}@-webkit-keyframes bounceRight-data-v-8633bda0{0%,to{-webkit-transform:translateY(0)}50%{-webkit-transform:translateX(10px)}}@keyframes bounceRight-data-v-8633bda0{0%,to{transform:translateY(0)}50%{transform:translateX(10px)}}.slide-fade-enter-active{transition:all .6s ease}.slide-fade-enter,.slide-fade-leave-to{transform:translateX(10px);opacity:0}

View File

@@ -1 +1 @@
.shake[data-v-54948e30]{-webkit-animation:shake-data-v-54948e30 .82s cubic-bezier(.36,.07,.19,.97) both;animation:shake-data-v-54948e30 .82s cubic-bezier(.36,.07,.19,.97) both;transform:translateZ(0);-webkit-backface-visibility:hidden;backface-visibility:hidden;perspective:1000px}@-webkit-keyframes shake-data-v-54948e30{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}}@keyframes shake-data-v-54948e30{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}} .touchable[data-v-18b1d8a0]{padding-right:2em;padding-left:2em;margin-right:-2em;margin-left:-2em}.shake[data-v-94120e12]{-webkit-animation:shake-data-v-94120e12 .82s cubic-bezier(.36,.07,.19,.97) both;animation:shake-data-v-94120e12 .82s cubic-bezier(.36,.07,.19,.97) both;transform:translateZ(0);-webkit-backface-visibility:hidden;backface-visibility:hidden;perspective:1000px}@-webkit-keyframes shake-data-v-94120e12{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}}@keyframes shake-data-v-94120e12{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}}

View File

@@ -0,0 +1 @@
.touchable[data-v-18b1d8a0]{padding-right:2em;padding-left:2em;margin-right:-2em;margin-left:-2em}

View File

@@ -0,0 +1 @@
.touchable[data-v-18b1d8a0]{padding-right:2em;padding-left:2em;margin-right:-2em;margin-left:-2em}

View File

@@ -1 +1 @@
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Vue App</title><link href="css/chunk-vendors.css" rel="preload" as="style"><link href="js/chunk-vendors.js" rel="preload" as="script"><link href="js/recipe_search_view.js" rel="preload" as="script"><link href="css/chunk-vendors.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="32x32" href="img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="img/icons/favicon-16x16.png"><link rel="manifest" href="manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black"><meta name="apple-mobile-web-app-title" content="Recipes"><link rel="apple-touch-icon" href="img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><div id="app"></div><script src="js/chunk-vendors.js"></script></body></html> <!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Vue App</title><link href="css/chunk-vendors.css" rel="preload" as="style"><link href="css/recipe_search_view.css" rel="preload" as="style"><link href="js/chunk-vendors.js" rel="preload" as="script"><link href="js/recipe_search_view.js" rel="preload" as="script"><link href="css/chunk-vendors.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="32x32" href="img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="img/icons/favicon-16x16.png"><link rel="manifest" href="manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black"><meta name="apple-mobile-web-app-title" content="Recipes"><link rel="apple-touch-icon" href="img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><div id="app"></div><script src="js/chunk-vendors.js"></script></body></html>

View File

@@ -1 +1 @@
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Vue App</title><link href="css/chunk-vendors.css" rel="preload" as="style"><link href="js/chunk-vendors.js" rel="preload" as="script"><link href="js/recipe_view.js" rel="preload" as="script"><link href="css/chunk-vendors.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="32x32" href="img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="img/icons/favicon-16x16.png"><link rel="manifest" href="manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black"><meta name="apple-mobile-web-app-title" content="Recipes"><link rel="apple-touch-icon" href="img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><div id="app"></div><script src="js/chunk-vendors.js"></script></body></html> <!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Vue App</title><link href="css/chunk-vendors.css" rel="preload" as="style"><link href="css/recipe_view.css" rel="preload" as="style"><link href="js/chunk-vendors.js" rel="preload" as="script"><link href="js/recipe_view.js" rel="preload" as="script"><link href="css/chunk-vendors.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="32x32" href="img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="img/icons/favicon-16x16.png"><link rel="manifest" href="manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black"><meta name="apple-mobile-web-app-title" content="Recipes"><link rel="apple-touch-icon" href="img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><div id="app"></div><script src="js/chunk-vendors.js"></script></body></html>

View File

@@ -18,6 +18,7 @@
<form method="POST" class="post-form">{% csrf_token %} <form method="POST" class="post-form">{% csrf_token %}
{{ form|crispy }} {{ form|crispy }}
<input type="submit" value="Submit" class="btn btn-success"> <input type="submit" value="Submit" class="btn btn-success">
<a href="{% url 'list_storage' %}"><button type="button" class="btn btn-primary">{% trans 'Manage External Storage' %}</button></a>
</form> </form>
</div> </div>
@@ -25,6 +26,8 @@
<br/> <br/>
<a href="{% url 'data_sync_wait' %}" class="btn btn-warning">{% trans 'Sync Now!' %}</a> <a href="{% url 'data_sync_wait' %}" class="btn btn-warning">{% trans 'Sync Now!' %}</a>
<a href="{% url 'list_recipe_import' %}" class="btn btn-info">{% trans 'Show Recipes' %}</a>
<a href="{% url 'list_sync_log' %}" class="btn btn-secondary">{% trans 'Show Log' %}</a>
<br/><br/> <br/><br/>
{% render_table monitored_paths %} {% render_table monitored_paths %}

View File

@@ -8,6 +8,15 @@
{% block content %} {% block content %}
{% if request.resolver_match.url_name in 'list_storage,list_recipe_import,list_sync_log' %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'data_sync' %}">{% trans 'External Recipes' %}</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ title }}</li>
</ol>
</nav>
{% endif %}
<div class="table-container"> <div class="table-container">
<h3 style="margin-bottom: 2vh">{{ title }} {% trans 'List' %} <h3 style="margin-bottom: 2vh">{{ title }} {% trans 'List' %}
{% if create_url %} {% if create_url %}

View File

@@ -10,7 +10,7 @@ from cookbook.helper import dal
from .models import (Comment, Food, InviteLink, Keyword, MealPlan, Recipe, from .models import (Comment, Food, InviteLink, Keyword, MealPlan, Recipe,
RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList, RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList,
Storage, Sync, SyncLog, Unit, get_model_name) Storage, Supermarket, SupermarketCategory, Sync, SyncLog, Unit, get_model_name)
from .views import api, data, delete, edit, import_export, lists, new, views, telegram from .views import api, data, delete, edit, import_export, lists, new, views, telegram
router = routers.DefaultRouter() router = routers.DefaultRouter()
@@ -176,7 +176,7 @@ for m in generic_models:
) )
) )
vue_models = [Food, Keyword, Unit] vue_models = [Food, Keyword, Unit, Supermarket, SupermarketCategory]
for m in vue_models: for m in vue_models:
py_name = get_model_name(m) py_name = get_model_name(m)
url_name = py_name.replace('_', '-') url_name = py_name.replace('_', '-')

View File

@@ -359,7 +359,7 @@ class SupermarketCategoryRelationViewSet(viewsets.ModelViewSet, StandardFilterMi
pagination_class = DefaultPagination pagination_class = DefaultPagination
def get_queryset(self): def get_queryset(self):
self.queryset = self.queryset.filter(supermarket__space=self.request.space) self.queryset = self.queryset.filter(supermarket__space=self.request.space).order_by('order')
return super().get_queryset() return super().get_queryset()

View File

@@ -146,7 +146,39 @@ def unit(request):
"title": _("Units"), "title": _("Units"),
"config": { "config": {
'model': "UNIT", # *REQUIRED* name of the model in models.js 'model': "UNIT", # *REQUIRED* name of the model in models.js
'recipe_param': 'units' # *OPTIONAL* name of the listRecipes parameter if filtering on this attribute 'recipe_param': 'units', # *OPTIONAL* name of the listRecipes parameter if filtering on this attribute
}
}
)
@group_required('user')
def supermarket(request):
# recipe-param is the name of the parameters used when filtering recipes by this attribute
# model-name is the models.js name of the model, probably ALL-CAPS
return render(
request,
'generic/model_template.html',
{
"title": _("Supermarkets"),
"config": {
'model': "SUPERMARKET", # *REQUIRED* name of the model in models.js
}
}
)
@group_required('user')
def supermarket_category(request):
# recipe-param is the name of the parameters used when filtering recipes by this attribute
# model-name is the models.js name of the model, probably ALL-CAPS
return render(
request,
'generic/model_template.html',
{
"title": _("Shopping Categories"),
"config": {
'model': "SHOPPING_CATEGORY", # *REQUIRED* name of the model in models.js
} }
} }
) )

13
vue/package-lock.json generated
View File

@@ -24,6 +24,7 @@
"vue-multiselect": "^2.1.6", "vue-multiselect": "^2.1.6",
"vue-property-decorator": "^9.1.2", "vue-property-decorator": "^9.1.2",
"vue-template-compiler": "^2.6.14", "vue-template-compiler": "^2.6.14",
"vue2-touch-events": "^3.2.2",
"vuedraggable": "^2.24.3", "vuedraggable": "^2.24.3",
"vuex": "^3.6.0", "vuex": "^3.6.0",
"workbox-webpack-plugin": "^6.1.5" "workbox-webpack-plugin": "^6.1.5"
@@ -14312,6 +14313,11 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/vue2-touch-events": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/vue2-touch-events/-/vue2-touch-events-3.2.2.tgz",
"integrity": "sha512-rGV8jxgOQEJYkJCp7uOBe3hjvmG1arThrq1wGtJHwJTgi65+P2a+0l4CYcQO/U1ZFqTq2/TT2+oTE6H7Y+6Eog=="
},
"node_modules/vuedraggable": { "node_modules/vuedraggable": {
"version": "2.24.3", "version": "2.24.3",
"license": "MIT", "license": "MIT",
@@ -17200,7 +17206,6 @@
"version": "4.5.13", "version": "4.5.13",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/core": "^7.11.0",
"@babel/helper-compilation-targets": "^7.9.6", "@babel/helper-compilation-targets": "^7.9.6",
"@babel/helper-module-imports": "^7.8.3", "@babel/helper-module-imports": "^7.8.3",
"@babel/plugin-proposal-class-properties": "^7.8.3", "@babel/plugin-proposal-class-properties": "^7.8.3",
@@ -17213,7 +17218,6 @@
"@vue/babel-plugin-jsx": "^1.0.3", "@vue/babel-plugin-jsx": "^1.0.3",
"@vue/babel-preset-jsx": "^1.2.4", "@vue/babel-preset-jsx": "^1.2.4",
"babel-plugin-dynamic-import-node": "^2.3.3", "babel-plugin-dynamic-import-node": "^2.3.3",
"core-js": "^3.6.5",
"core-js-compat": "^3.6.5", "core-js-compat": "^3.6.5",
"semver": "^6.1.0" "semver": "^6.1.0"
} }
@@ -25155,6 +25159,11 @@
"version": "1.9.1", "version": "1.9.1",
"dev": true "dev": true
}, },
"vue2-touch-events": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/vue2-touch-events/-/vue2-touch-events-3.2.2.tgz",
"integrity": "sha512-rGV8jxgOQEJYkJCp7uOBe3hjvmG1arThrq1wGtJHwJTgi65+P2a+0l4CYcQO/U1ZFqTq2/TT2+oTE6H7Y+6Eog=="
},
"vuedraggable": { "vuedraggable": {
"version": "2.24.3", "version": "2.24.3",
"requires": { "requires": {

View File

@@ -1,6 +1,5 @@
<template> <template>
<div id="app" style="margin-bottom: 4vh"> <div id="app" style="margin-bottom: 4vh" v-if="this_model">
<!-- v-if prevents component from loading before this_model has been assigned -->
<generic-modal-form v-if="this_model" <generic-modal-form v-if="this_model"
:model="this_model" :model="this_model"
:action="this_action" :action="this_action"
@@ -8,30 +7,74 @@
:item2="this_target" :item2="this_target"
:show="show_modal" :show="show_modal"
@finish-action="finishAction"/> @finish-action="finishAction"/>
<generic-split-lists v-if="this_model"
:list_name="this_model.name" <div class="row">
:right_counts="right_counts" <div class="col-md-2 d-none d-md-block">
:left_counts="left_counts" </div>
@reset="resetList" <div class="col-xl-8 col-12">
@get-list="getItems" <div class="container-fluid d-flex flex-column flex-grow-1">
@item-action="startAction"> <div class="row">
<template v-slot:cards-left> <div class="col-md-6" 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><b-button variant="link" size="lg" @click="startAction({'action':'new'})"><i class="fas fa-plus-circle"></i></b-button></span>
</h3>
</div>
<div class="col-md-3" />
<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')"/>
</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 <generic-horizontal-card
v-for="i in items_left" v-bind:key="i.id" v-for="i in items_left" v-bind:key="i.id"
:item=i :item=i
:model="this_model" :model="this_model"
:draggable="true"
@item-action="startAction($event, 'left')"/> @item-action="startAction($event, 'left')"/>
</template> </template>
<template v-slot:cards-right> </generic-infinite-cards>
<generic-horizontal-card v-for="i in items_right" v-bind:key="i.id" </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 :item=i
:model="this_model" :model="this_model"
:draggable="true"
@item-action="startAction($event, 'right')"/> @item-action="startAction($event, 'right')"/>
</template> </template>
</generic-split-lists> </generic-infinite-cards>
</div>
</div>
</div>
</div>
</div> </div>
</template> </template>
@@ -46,9 +89,10 @@ import 'bootstrap-vue/dist/bootstrap-vue.css'
import {CardMixin, ApiMixin} from "@/utils/utils"; import {CardMixin, ApiMixin} from "@/utils/utils";
import {StandardToasts, ToastMixin} from "@/utils/utils"; import {StandardToasts, ToastMixin} from "@/utils/utils";
import GenericSplitLists from "@/components/GenericSplitLists"; import GenericInfiniteCards from "@/components/GenericInfiniteCards";
import GenericHorizontalCard from "@/components/GenericHorizontalCard"; import GenericHorizontalCard from "@/components/GenericHorizontalCard";
import GenericModalForm from "@/components/Modals/GenericModalForm"; import GenericModalForm from "@/components/Modals/GenericModalForm";
import ModelMenu from "@/components/ModelMenu";
Vue.use(BootstrapVue) Vue.use(BootstrapVue)
@@ -57,7 +101,7 @@ export default {
// or i'm capturing it incorrectly // or i'm capturing it incorrectly
name: 'ModelListView', name: 'ModelListView',
mixins: [CardMixin, ApiMixin, ToastMixin], mixins: [CardMixin, ApiMixin, ToastMixin],
components: {GenericHorizontalCard, GenericSplitLists, GenericModalForm}, components: {GenericHorizontalCard, GenericModalForm, GenericInfiniteCards, ModelMenu},
data() { data() {
return { return {
// this.Models and this.Actions inherited from ApiMixin // this.Models and this.Actions inherited from ApiMixin
@@ -66,11 +110,14 @@ export default {
right_counts: {'max': 9999, 'current': 0}, right_counts: {'max': 9999, 'current': 0},
left_counts: {'max': 9999, 'current': 0}, left_counts: {'max': 9999, 'current': 0},
this_model: undefined, this_model: undefined,
model_menu: undefined,
this_action: undefined, this_action: undefined,
this_recipe_param: undefined, this_recipe_param: undefined,
this_item: {}, this_item: {},
this_target: {}, this_target: {},
show_modal: false show_modal: false,
show_split: false,
paginated: false,
} }
}, },
mounted() { mounted() {
@@ -78,6 +125,13 @@ export default {
let model_config = JSON.parse(document.getElementById('model_config').textContent) let model_config = JSON.parse(document.getElementById('model_config').textContent)
this.this_model = this.Models[model_config?.model] this.this_model = this.Models[model_config?.model]
this.this_recipe_param = model_config?.recipe_param this.this_recipe_param = model_config?.recipe_param
this.paginated = this.this_model?.paginated ?? false
this.$nextTick(() => {
if (!this.paginated) {
this.getItems()
}
})
}, },
methods: { methods: {
// this.genericAPI inherited from ApiMixin // this.genericAPI inherited from ApiMixin
@@ -165,13 +219,14 @@ export default {
} }
this.clearState() this.clearState()
}, },
getItems: function (params) { getItems: function (params, col) {
let column = params?.column ?? 'left' let column = col || 'left'
this.genericAPI(this.this_model, this.Actions.LIST, params).then((result) => { this.genericAPI(this.this_model, this.Actions.LIST, params).then((result) => {
if (result.data.results.length) { let results = result.data?.results ?? result.data
this['items_' + column] = this['items_' + column].concat(result.data?.results) if (results?.length) {
this[column + '_counts']['max'] = result.data.count this['items_' + column] = this['items_' + column].concat(results)
this[column + '_counts']['current'] = this['items_' + column].length this[column + '_counts']['max'] = result.data?.count ?? 0
this[column + '_counts']['current'] = this['items_' + column]?.length
} else { } else {
this[column + '_counts']['max'] = 0 this[column + '_counts']['max'] = 0
this[column + '_counts']['current'] = 0 this[column + '_counts']['current'] = 0

View File

@@ -155,7 +155,6 @@
<script> <script>
import Vue from 'vue' import Vue from 'vue'
import {BootstrapVue} from 'bootstrap-vue' import {BootstrapVue} from 'bootstrap-vue'
import 'bootstrap-vue/dist/bootstrap-vue.css' import 'bootstrap-vue/dist/bootstrap-vue.css'
import {apiLoadRecipe} from "@/utils/api"; import {apiLoadRecipe} from "@/utils/api";

View File

@@ -1,34 +1,39 @@
<template> <template>
<div row style="margin: 4px"> <div row style="margin: 4px">
<b-card no-body d-flex flex-column :class="{'border border-primary' : over, 'shake': isError}" <b-card no-body d-flex flex-column :class="{'border border-primary' : over, 'shake': isError}"
style="height: 10vh;" :style="{'cursor:grab' : draggable}" :style="{'cursor:grab' : useDrag}"
@dragover.prevent @dragover.prevent
@dragenter.prevent @dragenter.prevent
:draggable="draggable" :draggable="useDrag"
@dragstart="handleDragStart($event)" @dragstart="handleDragStart($event)"
@dragenter="handleDragEnter($event)" @dragenter="handleDragEnter($event)"
@dragleave="handleDragLeave($event)" @dragleave="handleDragLeave($event)"
@drop="handleDragDrop($event)"> @drop="handleDragDrop($event)">
<b-row no-gutters style="height:inherit;"> <b-row no-gutters >
<b-col no-gutters md="3" style="height:inherit;"> <b-col no-gutters class="col-sm-3">
<b-card-img-lazy style="object-fit: cover; height: 10vh;" :src="item_image" v-bind:alt="$t('Recipe_Image')"></b-card-img-lazy> <b-card-img-lazy style="object-fit: cover; height: 6em;" :src="item_image" v-bind:alt="$t('Recipe_Image')"></b-card-img-lazy>
</b-col> </b-col>
<b-col no-gutters md="9" style="height:inherit;"> <b-col no-gutters class="col-sm-9">
<b-card-body class="m-0 py-0" style="height:inherit;"> <b-card-body class="m-0 py-0">
<b-card-text class=" h-100 my-0 d-flex flex-column" style="text-overflow: ellipsis"> <b-card-text class=" h-100 my-0 d-flex flex-column" style="text-overflow: ellipsis">
<h5 class="m-0 mt-1 text-truncate">{{ item[title] }}</h5> <h5 class="m-0 mt-1 text-truncate">{{ item[title] }}</h5>
<div class= "m-0 text-truncate">{{ item[subtitle] }}</div> <div class= "m-0 text-truncate">{{ item[subtitle] }}</div>
<div class="mt-auto mb-1 d-flex flex-row justify-content-end"> <!-- <span>{{this_item[itemTags.field]}}</span> -->
<div v-if="item[child_count]" class="mx-2 btn btn-link btn-sm" <generic-pill v-for="x in itemTags" :key="x.field"
:item_list="item[x.field]"
:label="x.label"
:color="x.color"/>
<div class="mt-auto mb-1" align="right">
<span v-if="item[child_count]" class="mx-2 btn btn-link btn-sm"
style="z-index: 800;" v-on:click="$emit('item-action',{'action':'get-children','source':item})"> style="z-index: 800;" v-on:click="$emit('item-action',{'action':'get-children','source':item})">
<div v-if="!item.show_children">{{ item[child_count] }} {{ itemName }}</div> <div v-if="!item.show_children">{{ item[child_count] }} {{ itemName }}</div>
<div v-else>{{ text.hide_children }}</div> <div v-else>{{ text.hide_children }}</div>
</div> </span>
<div v-if="item[recipe_count]" class="mx-2 btn btn-link btn-sm" style="z-index: 800;" <span v-if="item[recipe_count]" class="mx-2 btn btn-link btn-sm" style="z-index: 800;"
v-on:click="$emit('item-action',{'action':'get-recipes','source':item})"> v-on:click="$emit('item-action',{'action':'get-recipes','source':item})">
<div v-if="!item.show_recipes">{{ item[recipe_count] }} {{$t('Recipes')}}</div> <div v-if="!item.show_recipes">{{ item[recipe_count] }} {{$t('Recipes')}}</div>
<div v-else>{{$t('Hide_Recipes')}}</div> <div v-else>{{$t('Hide_Recipes')}}</div>
</div> </span>
</div> </div>
</b-card-text> </b-card-text>
</b-card-body> </b-card-body>
@@ -45,24 +50,17 @@
</b-card> </b-card>
<!-- recursively add child cards --> <!-- recursively add child cards -->
<div class="row" v-if="item.show_children"> <div class="row" v-if="item.show_children">
<div class="col-md-11 offset-md-1"> <div class="col-md-10 offset-md-2">
<generic-horizontal-card v-for="child in item[children]" v-bind:key="child.id" <generic-horizontal-card v-for="child in item[children]" v-bind:key="child.id"
:draggable="draggable"
:item="child" :item="child"
:model="model" :model="model"
:title="title"
:subtitle="subtitle"
:child_count="child_count"
:children="children"
:recipe_count="recipe_count"
:recipes="recipes"
@item-action="$emit('item-action', $event)"> @item-action="$emit('item-action', $event)">
</generic-horizontal-card> </generic-horizontal-card>
</div> </div>
</div> </div>
<!-- conditionally view recipes --> <!-- conditionally view recipes -->
<div class="row" v-if="item.show_recipes"> <div class="row" v-if="item.show_recipes">
<div class="col-md-11 offset-md-1"> <div class="col-md-10 offset-md-2">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));grid-gap: 1rem;"> <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));grid-gap: 1rem;">
<recipe-card v-for="r in item[recipes]" <recipe-card v-for="r in item[recipes]"
v-bind:key="r.id" v-bind:key="r.id"
@@ -91,19 +89,19 @@
<script> <script>
import GenericContextMenu from "@/components/GenericContextMenu"; import GenericContextMenu from "@/components/GenericContextMenu";
import Badges from "@/components/Badges"; import Badges from "@/components/Badges";
import GenericPill from "@/components/GenericPill";
import RecipeCard from "@/components/RecipeCard"; import RecipeCard from "@/components/RecipeCard";
import { mixin as clickaway } from 'vue-clickaway'; import { mixin as clickaway } from 'vue-clickaway';
import { createPopper } from '@popperjs/core'; import { createPopper } from '@popperjs/core';
export default { export default {
name: "GenericHorizontalCard", name: "GenericHorizontalCard",
components: { GenericContextMenu, RecipeCard, Badges}, components: { GenericContextMenu, RecipeCard, Badges, GenericPill},
mixins: [clickaway], mixins: [clickaway],
props: { props: {
item: {type: Object}, item: {type: Object},
model: {type: Object}, model: {type: Object},
draggable: {type: Boolean, default: false}, title: {type: String, default: 'name'}, // this and the following props need to be moved to model.js and made computed values
title: {type: String, default: 'name'}, // this and the following props can probably be moved to model.js and made computed values
subtitle: {type: String, default: 'description'}, subtitle: {type: String, default: 'description'},
child_count: {type: String, default: 'numchild'}, child_count: {type: String, default: 'numchild'},
children: {type: String, default: 'children'}, children: {type: String, default: 'children'},
@@ -134,10 +132,16 @@ export default {
return this.model?.name ?? "You Forgot To Set Model Name in model.js" return this.model?.name ?? "You Forgot To Set Model Name in model.js"
}, },
useMove: function() { useMove: function() {
return this.model['move'] !== false return (this.model?.['move'] ?? false) ? true : false
}, },
useMerge: function() { useMerge: function() {
return this.model['merge'] !== false return (this.model?.['merge'] ?? false) ? true : false
},
useDrag: function() {
return this.useMove || this.useMerge
},
itemTags: function() {
return this.model?.tags ?? []
} }
}, },
methods: { methods: {

View File

@@ -0,0 +1,92 @@
<template>
<div id="app" style="margin-bottom: 4vh">
<div class="row flex-shrink-0">
<div class="col col-md">
<!-- search box -->
<b-input-group class="mt-3">
<b-input class="form-control" type="search" v-model="search_left"
v-bind:placeholder="this.text.search"></b-input>
</b-input-group>
</div>
</div>
<div class="row" :class="{'vh-100 mh-100 overflow-auto' : scroll}">
<div class="col col-md">
<slot name="cards"></slot>
<infinite-loading
:identifier='column'
@infinite="infiniteHandler"
spinner="waveDots">
<template v-slot:no-more><span/></template>
<template v-slot:no-results><span>{{$t('No_Results')}}</span></template>
</infinite-loading>
</div>
</div>
</div>
</template>
<script>
import 'bootstrap-vue/dist/bootstrap-vue.css'
import _debounce from 'lodash/debounce'
import InfiniteLoading from 'vue-infinite-loading';
export default {
name: 'GenericInfiniteCards',
components: {InfiniteLoading},
props: {
card_list: {type: Array, default(){return []}},
card_counts: {type: Object},
scroll: {type:Boolean, default: false}
},
data() {
return {
search: '',
page: 0,
state: undefined,
column: +new Date(),
text: {
'new': '',
'search': this.$t('Search')
},
}
},
mounted() {
},
watch: {
search: _debounce(function() {
this.page = 0
this.$emit('reset')
this.column += 1
}, 700),
card_counts: {
deep: true,
handler(newVal, oldVal) {
if (newVal.current > 0) {
this.state.loaded()
}
if (newVal.current >= newVal.max) {
this.state.complete()
}
}
},
},
methods: {
infiniteHandler: function($state, col) {
let params = {
'query': this.search,
'page': this.page + 1
}
this.state = $state
this.$emit('search', params)
this.page+= 1
},
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,44 @@
<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>
</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]
}
},
},
mounted() {
},
methods: {
thisLabel: function (item) {
let fields = this.label.split('::')
let value = item
fields.forEach(x => {
value = value[x]
});
return value
}
}
}
</script>

View File

@@ -1,220 +0,0 @@
<template>
<div id="app" style="margin-bottom: 4vh">
<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" :class="{'vh-100' : show_split}">
<!-- expanded options box -->
<div class="row flex-shrink-0">
<div class="col col-md-12">
<b-collapse id="collapse_advanced" class="mt-2" v-model="advanced_visible">
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-md-3" style="margin-top: 1vh">
<div class="btn btn-primary btn-block text-uppercase" @click="$emit('item-action', {'action':'new'})">
{{ this.text.new }}
</div>
</div>
<div class="col-md-3" style="margin-top: 1vh">
<button class="btn btn-primary btn-block text-uppercase" @click="resetSearch">
{{ this.text.reset }}
</button>
</div>
<div class="col-md-3" style="position: relative; margin-top: 1vh">
<b-form-checkbox v-model="show_split" name="check-button"
class="shadow-none"
style="position:relative;top: 50%; transform: translateY(-50%);" switch>
{{ this.text.split }}
</b-form-checkbox>
</div>
</div>
</div>
</div>
</b-collapse>
</div>
</div>
<div class="row flex-shrink-0">
<!-- search box -->
<div class="col col-md">
<b-input-group class="mt-3">
<b-input class="form-control" v-model="search_left"
v-bind:placeholder="this.text.search"></b-input>
<b-input-group-append>
<b-button v-b-toggle.collapse_advanced variant="primary" class="shadow-none">
<i class="fas fa-caret-down" v-if="!advanced_visible"></i>
<i class="fas fa-caret-up" v-if="advanced_visible"></i>
</b-button>
</b-input-group-append>
</b-input-group>
</div>
<!-- split side search -->
<div class="col col-md" v-if="show_split">
<b-input-group class="mt-3">
<b-input class="form-control" v-model="search_right"
v-bind:placeholder="this.text.search"></b-input>
</b-input-group>
</div>
</div>
<!-- only show scollbars in split mode -->
<!-- TODO: weird behavior when switching to split mode, infinite scoll doesn't trigger if
bottom of page is in viewport can trigger by scrolling page (not column) up -->
<div class="row" :class="{'overflow-hidden' : show_split}">
<div class="col col-md" :class="{'mh-100 overflow-auto' : show_split}">
<slot name="cards-left"></slot>
<infinite-loading
:identifier='left'
@infinite="infiniteHandler($event, 'left')"
spinner="waveDots">
<template v-slot:no-more><span/></template>
<template v-slot:no-results><span>{{$t('No_Results')}}</span></template>
</infinite-loading>
</div>
<!-- right side cards -->
<div class="col col-md mh-100 overflow-auto" v-if="show_split">
<slot name="cards-right"></slot>
<infinite-loading
:identifier='right'
@infinite="infiniteHandler($event, 'right')"
spinner="waveDots">
<template v-slot:no-more><span/></template>
<template v-slot:no-results><span>{{$t('No_Results')}}</span></template>
</infinite-loading>
</div>
</div>
</div>
</div>
<div class="col-md-2 d-none d-md-block">
</div>
</div>
</div>
</template>
<script>
import 'bootstrap-vue/dist/bootstrap-vue.css'
import _debounce from 'lodash/debounce'
import InfiniteLoading from 'vue-infinite-loading';
export default {
// TODO: this should be simplified into a Generic Infinitely Scrolling List and added as two components when split lists desired
name: 'GenericSplitLists',
components: {InfiniteLoading},
props: {
list_name: {type: String, default: 'Blank List'}, // TODO update translations to handle plural translations
left_list: {type: Array, default(){return []}},
left_counts: {type: Object},
right_list: {type:Array, default(){return []}},
right_counts: {type: Object},
},
data() {
return {
advanced_visible: false,
show_split: false,
search_right: '',
search_left: '',
right_page: 0,
left_page: 0,
right_state: undefined,
left_state: undefined,
right_dirty: false,
left_dirty: false,
right: +new Date(),
left: +new Date(),
text: {
'new': '',
'name': '',
'reset': this.$t('Reset_Search'),
'split': this.$t('show_split_screen'),
'search': this.$t('Search')
},
}
},
mounted() {
this.dragMenu = this.$refs.tooltip
this.text.new = this.$t('New_' + this.list_name)
},
watch: {
search_left: _debounce(function() {
if (this.left_dirty) {
//prevents running twice if search is reset
this.left_dirty = false
return
}
this.left_page = 0
this.$emit('reset', {'column':'left'})
this.left += 1
}, 700),
search_right: _debounce(function(newVal, oldVal) {
if (this.right_dirty) {
//prevents running twice if search is reset
this.right_dirty = false
return
}
this.right_page = 0
this.$emit('reset', {'column':'right'})
this.right += 1
}, 700),
right_counts: {
deep: true,
handler(newVal, oldVal) {
if (newVal.current > 0) {
this.right_state.loaded()
}
if (newVal.current >= newVal.max) {
this.right_state.complete()
}
}
},
left_counts: {
deep: true,
handler(newVal, oldVal) {
if (newVal.current > 0) {
this.left_state.loaded()
}
if (newVal.current >= newVal.max) {
this.left_state.complete()
}
}
}
},
methods: {
resetSearch: function () {
this.right_dirty = true
this.search_right = ''
this.right_page = 0
this.right += 1
this.$emit('reset', {'column':'right'})
this.left_dirty = true
this.search_left = ''
this.left_page = 0
this.left += 1
this.$emit('reset', {'column':'left'})
},
infiniteHandler: function($state, col) {
let params = {
'query': (col==='left') ? this.search_left : this.search_right,
'page': (col==='left') ? this.left_page + 1 : this.right_page + 1,
'column': col
}
this[col+'_state'] = $state
this.$emit('get-list', params)
this[col+'_page'] += 1
},
}
}
</script>
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
<style>
</style>

View File

@@ -26,8 +26,11 @@
</td> </td>
<td v-if="detailed"> <td v-if="detailed">
<div v-if="ingredient.note"> <div v-if="ingredient.note">
<span v-b-popover.hover="ingredient.note" <span v-b-popover.hover="ingredient.note" v-if="ingredient.note.length > 15"
class="d-print-none"> <i class="far fa-comment"></i> class="d-print-none touchable"> <i class="far fa-comment"></i>
</span>
<span v-else>
{{ ingredient.note }}
</span> </span>
<div class="d-none d-print-block"> <div class="d-none d-print-block">
@@ -72,3 +75,13 @@ export default {
} }
} }
</script> </script>
<style scoped>
/* increase size of hover/touchable space without changing spacing */
.touchable {
padding-right: 2em;
padding-left: 2em;
margin-right: -2em;
margin-left: -2em;
}
</style>

View File

@@ -0,0 +1,61 @@
<template>
<!-- <b-button variant="link" size="sm" class="text-dark shadow-none"><i class="fas fa-chevron-down"></i></b-button> -->
<span>
<b-dropdown variant="link" toggle-class="text-decoration-none text-dark shadow-none" no-caret style="boundary:window">
<template #button-content>
<i class="fas fa-chevron-down">
</template>
<b-dropdown-item :href="resolveDjangoUrl('list_food')">
<i class="fas fa-leaf fa-fw"></i> {{ Models['FOOD'].name }}
</b-dropdown-item>
<b-dropdown-item :href="resolveDjangoUrl('list_keyword')">
<i class="fas fa-tags fa-fw"></i> {{ Models['KEYWORD'].name }}
</b-dropdown-item>
<b-dropdown-item :href="resolveDjangoUrl('list_unit')">
<i class="fas fa-balance-scale fa-fw"></i> {{ Models['UNIT'].name }}
</b-dropdown-item>
<b-dropdown-item :href="resolveDjangoUrl('list_supermarket')">
<i class="fas fa-store-alt fa-fw"></i> {{ Models['SUPERMARKET'].name }}
</b-dropdown-item>
<b-dropdown-item :href="resolveDjangoUrl('list_supermarket_category')">
<i class="fas fa-cubes fa-fw"></i> {{ Models['SHOPPING_CATEGORY'].name }}
</b-dropdown-item>
</b-dropdown>
</span>
</template>
<script>
import Vue from 'vue'
import {BootstrapVue} from 'bootstrap-vue'
import 'bootstrap-vue/dist/bootstrap-vue.css'
import {Models} from "@/utils/models";
import {ResolveUrlMixin} from "@/utils/utils";
Vue.use(BootstrapVue)
export default {
name: 'ModelMenu',
mixins: [ResolveUrlMixin],
data() {
return {
Models: Models
}
},
mounted() {
},
methods: {
gotoURL: function(model) {
return
}
}
}
</script>

View File

@@ -14,7 +14,7 @@
"all_fields_optional": "All fields are optional and can be left empty.", "all_fields_optional": "All fields are optional and can be left empty.",
"convert_internal": "Convert to internal recipe", "convert_internal": "Convert to internal recipe",
"show_only_internal": "Show only internal recipes", "show_only_internal": "Show only internal recipes",
"show_split_screen": "Show split view", "show_split_screen": "Split View",
"Log_Recipe_Cooking": "Log Recipe Cooking", "Log_Recipe_Cooking": "Log Recipe Cooking",
"External_Recipe_Image": "External Recipe Image", "External_Recipe_Image": "External Recipe Image",
@@ -46,7 +46,7 @@
"Edit_Recipe": "Edit Recipe", "Edit_Recipe": "Edit Recipe",
"Move_Keyword": "Move Keyword", "Move_Keyword": "Move Keyword",
"Merge_Keyword": "Merge Keyword", "Merge_Keyword": "Merge Keyword",
"Hide_Keywords": "Hide Keywords", "Hide_Keywords": "Hide Keyword",
"Hide_Recipes": "Hide Recipes", "Hide_Recipes": "Hide Recipes",
"Move_Up": "Move up", "Move_Up": "Move up",
"Move_Down": "Move down", "Move_Down": "Move down",

View File

@@ -84,7 +84,7 @@
"Parent": "Ouder", "Parent": "Ouder",
"move_confirmation": "Verplaats {child} naar ouder {parent}", "move_confirmation": "Verplaats {child} naar ouder {parent}",
"merge_confirmation": "Vervang {source} with {target}", "merge_confirmation": "Vervang {source} with {target}",
"move_selection": "Selecteer een ouder om {child} naar te verplaatsen.", "move_selection": "Selecteer een ouder {type} om {source} naar te verplaatsen.",
"merge_selection": "Vervang alle voorvallen van {source} door het type {type}.", "merge_selection": "Vervang alle voorvallen van {source} door het type {type}.",
"Root": "Bron", "Root": "Bron",
"show_split_screen": "Toon gesplitste weergave", "show_split_screen": "Toon gesplitste weergave",
@@ -97,5 +97,35 @@
"Advanced Search Settings": "Geavanceerde zoekinstellingen", "Advanced Search Settings": "Geavanceerde zoekinstellingen",
"Merge": "Voeg samen", "Merge": "Voeg samen",
"delete_confimation": "Weet je zeker dat je {kw} en zijn kinderen wil verwijderen?", "delete_confimation": "Weet je zeker dat je {kw} en zijn kinderen wil verwijderen?",
"Merge_Keyword": "Voeg Etiket samen" "Merge_Keyword": "Voeg Etiket samen",
"step_time_minutes": "Stap duur in minuten",
"confirm_delete": "Weet je zeker dat je dit {object} wil verwijderen?",
"Show_as_header": "Toon als koptekst",
"Hide_as_header": "Verberg als koptekst",
"Copy_template_reference": "Kopieer sjabloon verwijzing",
"Save_and_View": "Sla op & Bekijk",
"Edit_Recipe": "Bewerk Recept",
"Move_Up": "Verplaats omhoog",
"Move_Down": "Verplaats omlaag",
"Step_Name": "Stap Naam",
"Step_Type": "Stap Type",
"Make_Header": "Maak_Koptekst",
"Make_Ingredient": "Maak_Ingrediënt",
"Enable_Amount": "Schakel Hoeveelheid in",
"Disable_Amount": "Schakel Hoeveelheid uit",
"Add_Step": "Voeg Stap toe",
"Note": "Notitie",
"delete_confirmation": "Weet je zeker dat je {source} wil verwijderen?",
"Ignore_Shopping": "Negeer Boodschappen",
"Shopping_Category": "Boodschappen Categorie",
"Edit_Food": "Bewerk Eten",
"Move_Food": "Verplaats Eten",
"New_Food": "Nieuw Eten",
"Hide_Food": "Verberg Eten",
"Delete_Food": "Verwijder Eten",
"No_ID": "ID niet gevonden, verwijderen niet mogelijk.",
"Meal_Plan_Days": "Toekomstige maaltijdplannen",
"merge_title": "Voeg {type} samen",
"move_title": "Verplaats {type}",
"Food": "Eten"
} }

View File

@@ -62,9 +62,13 @@ export class Models {
'name': i18n.t('Food'), // *OPTIONAL* : parameters will be built model -> model_type -> default 'name': i18n.t('Food'), // *OPTIONAL* : parameters will be built model -> model_type -> default
'apiName': 'Food', // *REQUIRED* : the name that is used in api.ts for this model 'apiName': 'Food', // *REQUIRED* : the name that is used in api.ts for this model
'model_type': this.TREE, // *OPTIONAL* : model specific params for api, if not present will attempt modeltype_create then default_create 'model_type': this.TREE, // *OPTIONAL* : model specific params for api, if not present will attempt modeltype_create then default_create
'paginated': true,
'move': true,
'merge': true,
'badges': { 'badges': {
'linked_recipe': true 'linked_recipe': true,
}, },
'tags': [{'field': 'supermarket_category', 'label': 'name', 'color': 'info'}],
// REQUIRED: unordered array of fields that can be set during create // REQUIRED: unordered array of fields that can be set during create
'create': { 'create': {
// if not defined partialUpdate will use the same parameters, prepending 'id' // if not defined partialUpdate will use the same parameters, prepending 'id'
@@ -113,6 +117,9 @@ export class Models {
'name': i18n.t('Keyword'), // *OPTIONAL: parameters will be built model -> model_type -> default 'name': i18n.t('Keyword'), // *OPTIONAL: parameters will be built model -> model_type -> default
'apiName': 'Keyword', 'apiName': 'Keyword',
'model_type': this.TREE, 'model_type': this.TREE,
'paginated': true,
'move': true,
'merge': true,
'badges': { 'badges': {
'icon': true 'icon': true
}, },
@@ -146,6 +153,7 @@ export class Models {
static UNIT = { static UNIT = {
'name': i18n.t('Unit'), 'name': i18n.t('Unit'),
'apiName': 'Unit', 'apiName': 'Unit',
'paginated': true,
'create': { 'create': {
'params': [['name', 'description']], 'params': [['name', 'description']],
'form': { 'form': {
@@ -165,7 +173,7 @@ export class Models {
} }
} }
}, },
'move': false 'merge': true
} }
static SHOPPING_LIST = {} static SHOPPING_LIST = {}
static RECIPE_BOOK = { static RECIPE_BOOK = {
@@ -220,6 +228,53 @@ export class Models {
} }
}, },
} }
static SHOPPING_CATEGORY_RELATION = {
'name': i18n.t('Shopping_Category'),
'apiName': 'SupermarketCategory',
'create': {
'params': [['category', 'supermarket', 'order']],
'form': {
'name': {
'form_field': true,
'type': 'text',
'field': 'name',
'label': i18n.t('Name'),
'placeholder': ''
},
'description': {
'form_field': true,
'type': 'text',
'field': 'description',
'label': i18n.t('Description'),
'placeholder': ''
}
}
},
}
static SUPERMARKET = {
'name': i18n.t('Supermarket'),
'apiName': 'Supermarket',
'tags': [{'field': 'category_to_supermarket', 'label': 'category::name', 'color': 'info'}],
'create': {
'params': [['name', 'description', 'category_to_supermarket']],
'form': {
'name': {
'form_field': true,
'type': 'text',
'field': 'name',
'label': i18n.t('Name'),
'placeholder': ''
},
'description': {
'form_field': true,
'type': 'text',
'field': 'description',
'label': i18n.t('Description'),
'placeholder': ''
},
}
},
}
static RECIPE = { static RECIPE = {
'name': i18n.t('Recipe'), 'name': i18n.t('Recipe'),

View File

@@ -3,23 +3,23 @@
"assets": { "assets": {
"../../templates/sw.js": { "../../templates/sw.js": {
"name": "../../templates/sw.js", "name": "../../templates/sw.js",
"path": "..\\..\\templates\\sw.js" "path": "../../templates/sw.js"
}, },
"css/chunk-vendors.css": { "css/chunk-vendors.css": {
"name": "css/chunk-vendors.css", "name": "css/chunk-vendors.css",
"path": "css\\chunk-vendors.css" "path": "css/chunk-vendors.css"
}, },
"js/chunk-vendors.js": { "js/chunk-vendors.js": {
"name": "js/chunk-vendors.js", "name": "js/chunk-vendors.js",
"path": "js\\chunk-vendors.js" "path": "js/chunk-vendors.js"
}, },
"css/cookbook_view.css": { "css/cookbook_view.css": {
"name": "css/cookbook_view.css", "name": "css/cookbook_view.css",
"path": "css\\cookbook_view.css" "path": "css/cookbook_view.css"
}, },
"js/cookbook_view.js": { "js/cookbook_view.js": {
"name": "js/cookbook_view.js", "name": "js/cookbook_view.js",
"path": "js\\cookbook_view.js" "path": "js/cookbook_view.js"
}, },
"css/edit_internal_recipe.css": { "css/edit_internal_recipe.css": {
"name": "css/edit_internal_recipe.css", "name": "css/edit_internal_recipe.css",
@@ -31,35 +31,43 @@
}, },
"js/import_response_view.js": { "js/import_response_view.js": {
"name": "js/import_response_view.js", "name": "js/import_response_view.js",
"path": "js\\import_response_view.js" "path": "js/import_response_view.js"
}, },
"css/model_list_view.css": { "css/model_list_view.css": {
"name": "css/model_list_view.css", "name": "css/model_list_view.css",
"path": "css\\model_list_view.css" "path": "css/model_list_view.css"
}, },
"js/model_list_view.js": { "js/model_list_view.js": {
"name": "js/model_list_view.js", "name": "js/model_list_view.js",
"path": "js\\model_list_view.js" "path": "js/model_list_view.js"
}, },
"js/offline_view.js": { "js/offline_view.js": {
"name": "js/offline_view.js", "name": "js/offline_view.js",
"path": "js\\offline_view.js" "path": "js/offline_view.js"
},
"css/recipe_search_view.css": {
"name": "css/recipe_search_view.css",
"path": "css/recipe_search_view.css"
}, },
"js/recipe_search_view.js": { "js/recipe_search_view.js": {
"name": "js/recipe_search_view.js", "name": "js/recipe_search_view.js",
"path": "js\\recipe_search_view.js" "path": "js/recipe_search_view.js"
},
"css/recipe_view.css": {
"name": "css/recipe_view.css",
"path": "css/recipe_view.css"
}, },
"js/recipe_view.js": { "js/recipe_view.js": {
"name": "js/recipe_view.js", "name": "js/recipe_view.js",
"path": "js\\recipe_view.js" "path": "js/recipe_view.js"
}, },
"js/supermarket_view.js": { "js/supermarket_view.js": {
"name": "js/supermarket_view.js", "name": "js/supermarket_view.js",
"path": "js\\supermarket_view.js" "path": "js/supermarket_view.js"
}, },
"js/user_file_view.js": { "js/user_file_view.js": {
"name": "js/user_file_view.js", "name": "js/user_file_view.js",
"path": "js\\user_file_view.js" "path": "js/user_file_view.js"
}, },
"recipe_search_view.html": { "recipe_search_view.html": {
"name": "recipe_search_view.html", "name": "recipe_search_view.html",
@@ -106,11 +114,13 @@
"recipe_search_view": [ "recipe_search_view": [
"css/chunk-vendors.css", "css/chunk-vendors.css",
"js/chunk-vendors.js", "js/chunk-vendors.js",
"css/recipe_search_view.css",
"js/recipe_search_view.js" "js/recipe_search_view.js"
], ],
"recipe_view": [ "recipe_view": [
"css/chunk-vendors.css", "css/chunk-vendors.css",
"js/chunk-vendors.js", "js/chunk-vendors.js",
"css/recipe_view.css",
"js/recipe_view.js" "js/recipe_view.js"
], ],
"offline_view": [ "offline_view": [