fully integrated property editor
This commit is contained in:
parent
d1174ea50d
commit
3e083e2168
31
cookbook/templates/property_editor.html
Normal file
31
cookbook/templates/property_editor.html
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load render_bundle from webpack_loader %}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load l10n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans 'Property Editor' %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content_fluid %}
|
||||||
|
|
||||||
|
<div id="app">
|
||||||
|
<property-editor-view></property-editor-view>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
{% if debug %}
|
||||||
|
<script src="{% url 'js_reverse' %}"></script>
|
||||||
|
{% else %}
|
||||||
|
<script src="{% static 'django_js_reverse/reverse.js' %}"></script>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<script type="application/javascript">
|
||||||
|
window.RECIPE_ID = {{ recipe_id }}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% render_bundle 'property_editor_view' %}
|
||||||
|
{% endblock %}
|
@ -91,6 +91,7 @@ urlpatterns = [
|
|||||||
path('history/', views.history, name='view_history'),
|
path('history/', views.history, name='view_history'),
|
||||||
path('supermarket/', views.supermarket, name='view_supermarket'),
|
path('supermarket/', views.supermarket, name='view_supermarket'),
|
||||||
path('ingredient-editor/', views.ingredient_editor, name='view_ingredient_editor'),
|
path('ingredient-editor/', views.ingredient_editor, name='view_ingredient_editor'),
|
||||||
|
path('property-editor/<int:pk>', views.property_editor, name='view_property_editor'),
|
||||||
path('abuse/<slug:token>', views.report_share_abuse, name='view_report_share_abuse'),
|
path('abuse/<slug:token>', views.report_share_abuse, name='view_report_share_abuse'),
|
||||||
|
|
||||||
path('api/import/', api.import_files, name='view_import'),
|
path('api/import/', api.import_files, name='view_import'),
|
||||||
|
@ -204,6 +204,11 @@ def ingredient_editor(request):
|
|||||||
return render(request, 'ingredient_editor.html', template_vars)
|
return render(request, 'ingredient_editor.html', template_vars)
|
||||||
|
|
||||||
|
|
||||||
|
@group_required('user')
|
||||||
|
def property_editor(request, pk):
|
||||||
|
return render(request, 'property_editor.html', {'recipe_id': pk})
|
||||||
|
|
||||||
|
|
||||||
@group_required('guest')
|
@group_required('guest')
|
||||||
def shopping_settings(request):
|
def shopping_settings(request):
|
||||||
if request.space.demo:
|
if request.space.demo:
|
||||||
|
209
vue/src/apps/PropertyEditorView/PropertyEditorView.vue
Normal file
209
vue/src/apps/PropertyEditorView/PropertyEditorView.vue
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
<div id="app">
|
||||||
|
<div>
|
||||||
|
<div class="row" v-if="recipe" style="max-height: 10vh">
|
||||||
|
|
||||||
|
<div class="col col-8">
|
||||||
|
<h2><a :href="resolveDjangoUrl('view_recipe', recipe.id)">{{ recipe.name }}</a></h2>
|
||||||
|
{{ recipe.description }}
|
||||||
|
<keywords-component :recipe="recipe"></keywords-component>
|
||||||
|
</div>
|
||||||
|
<div class="col col-4" v-if="recipe.image">
|
||||||
|
<img style="max-height: 10vh" class="img-thumbnail float-right" :src="recipe.image">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row mt-5">
|
||||||
|
<div class="col col-12">
|
||||||
|
|
||||||
|
<table class="table table-sm table-bordered table-responsive">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td>{{ $t('Name') }}</td>
|
||||||
|
<td>FDC</td>
|
||||||
|
<td>{{ $t('Properties_Food_Amount') }}</td>
|
||||||
|
<td>{{ $t('Properties_Food_Unit') }}</td>
|
||||||
|
<td v-for="pt in property_types" v-bind:key="pt.id">
|
||||||
|
<b-button variant="primary" @click="editing_property_type = pt" class="btn-block">{{ pt.name }} <span v-if="pt.unit !== ''">({{ pt.unit }})</span></b-button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="f in this.foods" v-bind:key="f.id">
|
||||||
|
<td>
|
||||||
|
{{ f.name }}
|
||||||
|
</td>
|
||||||
|
<td style="width: 11em;">
|
||||||
|
<b-input-group>
|
||||||
|
<b-form-input v-model="f.fdc_id" type="number" @change="updateFood(f)" :disabled="f.loading"></b-form-input>
|
||||||
|
<b-input-group-append>
|
||||||
|
<b-button variant="success" @click="updateFoodFromFDC(f)" :disabled="f.loading"><i class="fas fa-sync-alt" :class="{'fa-spin': loading}"></i></b-button>
|
||||||
|
</b-input-group-append>
|
||||||
|
</b-input-group>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td style="width: 5em; ">
|
||||||
|
<b-input v-model="f.properties_food_amount" type="number" @change="updateFood(f)" :disabled="f.loading"></b-input>
|
||||||
|
</td>
|
||||||
|
<td style="width: 8em;">
|
||||||
|
<generic-multiselect
|
||||||
|
@change="f.properties_food_unit = $event.val; updateFood(f)"
|
||||||
|
:initial_single_selection="f.properties_food_unit"
|
||||||
|
label="name" :model="Models.UNIT"
|
||||||
|
:multiple="false"
|
||||||
|
:disabled="f.loading"/>
|
||||||
|
</td>
|
||||||
|
<td v-for="p in f.properties" v-bind:key="`${f.id}_${p.property_type.id}`">
|
||||||
|
<b-input-group>
|
||||||
|
<b-form-input v-model="p.property_amount" type="number" :disabled="f.loading" v-b-tooltip.focus :title="p.property_type.name" @change="updateFood(f)"></b-form-input>
|
||||||
|
</b-input-group>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<generic-modal-form
|
||||||
|
:show="editing_property_type !== null"
|
||||||
|
:model="Models.PROPERTY_TYPE"
|
||||||
|
:action="Actions.UPDATE"
|
||||||
|
:item1="editing_property_type"
|
||||||
|
@finish-action="editing_property_type = null; loadData()">
|
||||||
|
</generic-modal-form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Vue from "vue"
|
||||||
|
import {BootstrapVue} from "bootstrap-vue"
|
||||||
|
|
||||||
|
import "bootstrap-vue/dist/bootstrap-vue.css"
|
||||||
|
import {ApiMixin, resolveDjangoUrl, StandardToasts} from "@/utils/utils";
|
||||||
|
import axios from "axios";
|
||||||
|
import BetaWarning from "@/components/BetaWarning.vue";
|
||||||
|
import {ApiApiFactory} from "@/utils/openapi/api";
|
||||||
|
import GenericMultiselect from "@/components/GenericMultiselect.vue";
|
||||||
|
import GenericModalForm from "@/components/Modals/GenericModalForm.vue";
|
||||||
|
import KeywordsComponent from "@/components/KeywordsComponent.vue";
|
||||||
|
|
||||||
|
|
||||||
|
Vue.use(BootstrapVue)
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "PropertyEditorView",
|
||||||
|
mixins: [ApiMixin],
|
||||||
|
components: {KeywordsComponent, GenericModalForm, GenericMultiselect},
|
||||||
|
computed: {},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
recipe: null,
|
||||||
|
property_types: [],
|
||||||
|
editing_property_type: null,
|
||||||
|
loading: false,
|
||||||
|
foods: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$i18n.locale = window.CUSTOM_LOCALE
|
||||||
|
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resolveDjangoUrl,
|
||||||
|
loadData: function () {
|
||||||
|
let apiClient = new ApiApiFactory()
|
||||||
|
|
||||||
|
apiClient.listPropertyTypes().then(result => {
|
||||||
|
this.property_types = result.data
|
||||||
|
|
||||||
|
apiClient.retrieveRecipe(window.RECIPE_ID).then(result => {
|
||||||
|
this.recipe = result.data
|
||||||
|
|
||||||
|
this.foods = []
|
||||||
|
|
||||||
|
this.recipe.steps.forEach(s => {
|
||||||
|
s.ingredients.forEach(i => {
|
||||||
|
if (this.foods.filter(x => (x.id === i.food.id)).length === 0) {
|
||||||
|
this.foods.push(this.buildFood(i.food))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.loading = false;
|
||||||
|
}).catch((err) => {
|
||||||
|
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
|
||||||
|
})
|
||||||
|
}).catch((err) => {
|
||||||
|
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
buildFood: function (food) {
|
||||||
|
/**
|
||||||
|
* Prepare food for display in grid by making sure the food properties are in the same order as property_types and that no types are missing
|
||||||
|
* */
|
||||||
|
|
||||||
|
let existing_properties = {}
|
||||||
|
food.properties.forEach(fp => {
|
||||||
|
existing_properties[fp.property_type.id] = fp
|
||||||
|
})
|
||||||
|
|
||||||
|
let food_properties = []
|
||||||
|
this.property_types.forEach(pt => {
|
||||||
|
let new_food_property = {
|
||||||
|
property_type: pt,
|
||||||
|
property_amount: 0,
|
||||||
|
}
|
||||||
|
if (pt.id in existing_properties) {
|
||||||
|
new_food_property = existing_properties[pt.id]
|
||||||
|
}
|
||||||
|
food_properties.push(new_food_property)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$set(food, 'loading', false)
|
||||||
|
|
||||||
|
food.properties = food_properties
|
||||||
|
|
||||||
|
return food
|
||||||
|
},
|
||||||
|
spliceInFood: function (food) {
|
||||||
|
/**
|
||||||
|
* replace food in foods list, for example after updates from the server
|
||||||
|
*/
|
||||||
|
this.foods = this.foods.map(f => (f.id === food.id) ? food : f)
|
||||||
|
|
||||||
|
},
|
||||||
|
updateFood: function (food) {
|
||||||
|
let apiClient = new ApiApiFactory()
|
||||||
|
apiClient.partialUpdateFood(food.id, food).then(result => {
|
||||||
|
this.spliceInFood(this.buildFood(result.data))
|
||||||
|
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
|
||||||
|
}).catch((err) => {
|
||||||
|
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateFoodFromFDC: function (food) {
|
||||||
|
food.loading = true;
|
||||||
|
let apiClient = new ApiApiFactory()
|
||||||
|
|
||||||
|
apiClient.fdcFood(food.id).then(result => {
|
||||||
|
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
|
||||||
|
this.spliceInFood(this.buildFood(result.data))
|
||||||
|
}).catch((err) => {
|
||||||
|
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
|
||||||
|
food.loading = false;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
22
vue/src/apps/PropertyEditorView/main.js
Normal file
22
vue/src/apps/PropertyEditorView/main.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import App from './PropertyEditorView.vue'
|
||||||
|
import i18n from '@/i18n'
|
||||||
|
import {createPinia, PiniaVuePlugin} from "pinia";
|
||||||
|
|
||||||
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
|
// TODO move this and other default stuff to centralized JS file (verify nothing breaks)
|
||||||
|
let publicPath = localStorage.STATIC_URL + 'vue/'
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
publicPath = 'http://localhost:8080/'
|
||||||
|
}
|
||||||
|
export default __webpack_public_path__ = publicPath // eslint-disable-line
|
||||||
|
|
||||||
|
Vue.use(PiniaVuePlugin)
|
||||||
|
const pinia = createPinia()
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
pinia,
|
||||||
|
i18n,
|
||||||
|
render: h => h(App),
|
||||||
|
}).$mount('#app')
|
@ -10,6 +10,9 @@
|
|||||||
<a class="dropdown-item" :href="resolveDjangoUrl('edit_recipe', recipe.id)" v-if="!disabled_options.edit"><i
|
<a class="dropdown-item" :href="resolveDjangoUrl('edit_recipe', recipe.id)" v-if="!disabled_options.edit"><i
|
||||||
class="fas fa-pencil-alt fa-fw"></i> {{ $t("Edit") }}</a>
|
class="fas fa-pencil-alt fa-fw"></i> {{ $t("Edit") }}</a>
|
||||||
|
|
||||||
|
<a class="dropdown-item" :href="resolveDjangoUrl('view_property_editor', recipe.id)" v-if="!disabled_options.edit">
|
||||||
|
<i class="fas fa-table"></i> {{ $t("Property_Editor") }}</a>
|
||||||
|
|
||||||
<a class="dropdown-item" :href="resolveDjangoUrl('edit_convert_recipe', recipe.id)"
|
<a class="dropdown-item" :href="resolveDjangoUrl('edit_convert_recipe', recipe.id)"
|
||||||
v-if="!recipe.internal && !disabled_options.convert"><i class="fas fa-exchange-alt fa-fw"></i> {{ $t("convert_internal") }}</a>
|
v-if="!recipe.internal && !disabled_options.convert"><i class="fas fa-exchange-alt fa-fw"></i> {{ $t("convert_internal") }}</a>
|
||||||
|
|
||||||
|
@ -185,6 +185,7 @@
|
|||||||
"move_title": "Move {type}",
|
"move_title": "Move {type}",
|
||||||
"Food": "Food",
|
"Food": "Food",
|
||||||
"Property": "Property",
|
"Property": "Property",
|
||||||
|
"Property_Editor": "Property Editor",
|
||||||
"Conversion": "Conversion",
|
"Conversion": "Conversion",
|
||||||
"Original_Text": "Original Text",
|
"Original_Text": "Original Text",
|
||||||
"Recipe_Book": "Recipe Book",
|
"Recipe_Book": "Recipe Book",
|
||||||
|
@ -53,6 +53,10 @@ const pages = {
|
|||||||
entry: "./src/apps/IngredientEditorView/main.js",
|
entry: "./src/apps/IngredientEditorView/main.js",
|
||||||
chunks: ["chunk-vendors","locales-chunk","api-chunk"],
|
chunks: ["chunk-vendors","locales-chunk","api-chunk"],
|
||||||
},
|
},
|
||||||
|
property_editor_view: {
|
||||||
|
entry: "./src/apps/PropertyEditorView/main.js",
|
||||||
|
chunks: ["chunk-vendors","locales-chunk","api-chunk"],
|
||||||
|
},
|
||||||
shopping_list_view: {
|
shopping_list_view: {
|
||||||
entry: "./src/apps/ShoppingListView/main.js",
|
entry: "./src/apps/ShoppingListView/main.js",
|
||||||
chunks: ["chunk-vendors","locales-chunk","api-chunk"],
|
chunks: ["chunk-vendors","locales-chunk","api-chunk"],
|
||||||
@ -137,7 +141,7 @@ module.exports = {
|
|||||||
|
|
||||||
config.optimization.minimize(true)
|
config.optimization.minimize(true)
|
||||||
|
|
||||||
//TODO somehow remov them as they are also added to the manifest config of the service worker
|
//TODO somehow remove them as they are also added to the manifest config of the service worker
|
||||||
/*
|
/*
|
||||||
Object.keys(pages).forEach(page => {
|
Object.keys(pages).forEach(page => {
|
||||||
config.plugins.delete(`html-${page}`);
|
config.plugins.delete(`html-${page}`);
|
||||||
|
Loading…
Reference in New Issue
Block a user