working WIP
This commit is contained in:
parent
d606ea8db3
commit
f7b2af7f97
@ -1 +1 @@
|
||||
.shake[data-v-997172e4]{-webkit-animation:shake-data-v-997172e4 .82s cubic-bezier(.36,.07,.19,.97) both;animation:shake-data-v-997172e4 .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-997172e4{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-997172e4{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)}}
|
||||
.shake[data-v-05ce8dfc]{-webkit-animation:shake-data-v-05ce8dfc .82s cubic-bezier(.36,.07,.19,.97) both;animation:shake-data-v-05ce8dfc .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-05ce8dfc{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-05ce8dfc{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)}}
|
@ -1 +1 @@
|
||||
.shake[data-v-997172e4]{-webkit-animation:shake-data-v-997172e4 .82s cubic-bezier(.36,.07,.19,.97) both;animation:shake-data-v-997172e4 .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-997172e4{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-997172e4{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)}}
|
||||
.shake[data-v-05ce8dfc]{-webkit-animation:shake-data-v-05ce8dfc .82s cubic-bezier(.36,.07,.19,.97) both;animation:shake-data-v-05ce8dfc .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-05ce8dfc{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-05ce8dfc{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)}}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
7963
vue/package-lock.json
generated
7963
vue/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -22,6 +22,7 @@
|
||||
"vue-cookies": "^1.7.4",
|
||||
"vue-i18n": "^8.24.4",
|
||||
"vue-infinite-loading": "^2.4.5",
|
||||
"vue-infinite-scroll": "^2.0.2",
|
||||
"vue-multiselect": "^2.1.6",
|
||||
"vue-property-decorator": "^9.1.2",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
|
@ -1,123 +1,47 @@
|
||||
<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="startAction({'action':'new'})">
|
||||
{{ this.$t('New_Food') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3" style="margin-top: 1vh">
|
||||
<button class="btn btn-primary btn-block text-uppercase" @click="resetSearch">
|
||||
{{ this.$t('Reset_Search') }}
|
||||
</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.$t('show_split_screen') }}
|
||||
</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_input"
|
||||
v-bind:placeholder="this.$t('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_input2"
|
||||
v-bind:placeholder="this.$t('Search')"></b-input>
|
||||
</b-input-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- only show scollbars in split mode, but this doesn't interact well with infinite scroll, maybe a different componenet? -->
|
||||
<div class="row" :class="{'overflow-hidden' : show_split}" style="margin-top: 2vh">
|
||||
<div class="col col-md" :class="{'mh-100 overflow-auto' : show_split}">
|
||||
<generic-horizontal-card v-for="f in foods" v-bind:key="f.id"
|
||||
:model=f
|
||||
model_name="Food"
|
||||
:draggable="true"
|
||||
:merge="true"
|
||||
:move="true"
|
||||
@item-action="startAction($event, 'left')"
|
||||
>
|
||||
<template v-slot:upper-right>
|
||||
<b-button v-if="f.recipe" v-b-tooltip.hover :title="f.recipe.name"
|
||||
class=" btn fas fa-book-open p-0 border-0" variant="link" :href="f.recipe.url"/>
|
||||
</template>
|
||||
</generic-horizontal-card>
|
||||
<infinite-loading
|
||||
:identifier='left'
|
||||
@infinite="infiniteHandler($event, 'left')"
|
||||
spinner="waveDots">
|
||||
<template v-slot:no-more><span/></template>
|
||||
</infinite-loading>
|
||||
</div>
|
||||
<!-- right side food cards -->
|
||||
<div class="col col-md mh-100 overflow-auto " v-if="show_split">
|
||||
<generic-horizontal-card v-for="f in foods" v-bind:key="f.id"
|
||||
:model=f
|
||||
model_name="Food"
|
||||
:draggable="true"
|
||||
:merge="true"
|
||||
:move="true"
|
||||
@item-action="startAction($event, 'left')"
|
||||
>
|
||||
<template v-slot:upper-right>
|
||||
<b-button v-if="f.recipe" v-b-tooltip.hover :title="f.recipe.name"
|
||||
class=" btn fas fa-book-open p-0 border-0" variant="link" :href="f.recipe.url"/>
|
||||
</template>
|
||||
</generic-horizontal-card>
|
||||
<infinite-loading
|
||||
:identifier='right'
|
||||
@infinite="infiniteHandler($event, 'right')"
|
||||
spinner="waveDots">
|
||||
<template v-slot:no-more><span/></template>
|
||||
</infinite-loading>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="col-md-2 d-none d-md-block">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<generic-split-lists
|
||||
:list_name="this_model"
|
||||
:load_more_left="load_more_left"
|
||||
:load_more_right="load_more_right"
|
||||
@reset="resetList"
|
||||
@get-list="getFoods"
|
||||
@item-action="startAction"
|
||||
>
|
||||
<template v-slot:cards-left>
|
||||
<generic-horizontal-card
|
||||
v-for="f in foods" v-bind:key="f.id"
|
||||
:model=f
|
||||
:model_name="this_model"
|
||||
:draggable="true"
|
||||
:tree="true"
|
||||
:merge="true"
|
||||
:move="true"
|
||||
@item-action="startAction($event, 'left')"
|
||||
>
|
||||
<template v-slot:upper-right>
|
||||
<b-button v-if="f.recipe" v-b-tooltip.hover :title="f.recipe.name"
|
||||
class=" btn fas fa-book-open p-0 border-0" variant="link" :href="f.recipe.url"/>
|
||||
</template>
|
||||
</generic-horizontal-card>
|
||||
</template>
|
||||
<template v-slot:cards-right>
|
||||
<generic-horizontal-card v-for="f in foods2" v-bind:key="f.id"
|
||||
:model=f
|
||||
:model_name="this_model"
|
||||
:draggable="true"
|
||||
:tree="true"
|
||||
:merge="true"
|
||||
:move="true"
|
||||
@item-action="startAction($event, 'right')"
|
||||
>
|
||||
<template v-slot:upper-right>
|
||||
<b-button v-if="f.recipe" v-b-tooltip.hover :title="f.recipe.name"
|
||||
class=" btn fas fa-book-open p-0 border-0" variant="link" :href="f.recipe.url"/>
|
||||
</template>
|
||||
</generic-horizontal-card>
|
||||
</template>
|
||||
</generic-split-lists>
|
||||
|
||||
<!-- TODO Modals can probably be made generic and moved to component -->
|
||||
<!-- edit modal -->
|
||||
@ -167,8 +91,8 @@
|
||||
:title="this.$t('Delete_Food')"
|
||||
:ok-title="this.$t('Delete')"
|
||||
:cancel-title="this.$t('Cancel')"
|
||||
@ok="delFood(this_item.id)">
|
||||
{{this.$t("delete_confimation", {'kw': this_item.name})}} {{this_item.name}}
|
||||
@ok="deleteThis(this_item.id, this_model)">
|
||||
{{this.$t("delete_confimation", {'kw': this_item.name})}}
|
||||
</b-modal>
|
||||
<!-- move modal -->
|
||||
<b-modal class="modal"
|
||||
@ -220,39 +144,29 @@ import Vue from 'vue'
|
||||
import {BootstrapVue} from 'bootstrap-vue'
|
||||
|
||||
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
||||
import _debounce from 'lodash/debounce'
|
||||
|
||||
import {ToastMixin} from "@/utils/utils";
|
||||
|
||||
import {ApiApiFactory} from "@/utils/openapi/api.ts";
|
||||
// import FoodCard from "@/components/FoodCard";
|
||||
import GenericSplitLists from "@/components/GenericSplitLists";
|
||||
import GenericHorizontalCard from "@/components/GenericHorizontalCard";
|
||||
import GenericMultiselect from "@/components/GenericMultiselect";
|
||||
import InfiniteLoading from 'vue-infinite-loading';
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
export default {
|
||||
name: 'FoodListView',
|
||||
mixins: [ToastMixin],
|
||||
components: {GenericHorizontalCard, GenericMultiselect, InfiniteLoading},
|
||||
mixins: [ToastMixin, GenericSplitLists, GenericHorizontalCard],
|
||||
components: {GenericHorizontalCard, GenericMultiselect, GenericSplitLists},
|
||||
data() {
|
||||
return {
|
||||
this_model: 'Food',
|
||||
foods: [],
|
||||
foods2: [],
|
||||
show_split: false,
|
||||
search_input: '',
|
||||
search_input2: '',
|
||||
advanced_visible: false,
|
||||
right_page: 0,
|
||||
right: +new Date(),
|
||||
isDirtyRight: false,
|
||||
left_page: 0,
|
||||
update_recipe: [],
|
||||
left: +new Date(),
|
||||
isDirtyLeft: false,
|
||||
this_item: {
|
||||
'id': -1,
|
||||
load_more_left: true,
|
||||
load_more_right: true,
|
||||
blank_item: {
|
||||
'id': undefined,
|
||||
'name': '',
|
||||
'description': '',
|
||||
'recipe': null,
|
||||
@ -260,85 +174,135 @@ export default {
|
||||
'ignore_shopping': false,
|
||||
'supermarket_category': undefined,
|
||||
'target': {
|
||||
'id': -1,
|
||||
'id': undefined,
|
||||
'name': ''
|
||||
},
|
||||
},
|
||||
this_item: {
|
||||
'id': undefined,
|
||||
'name': '',
|
||||
'description': '',
|
||||
'recipe': null,
|
||||
'recipe_full': undefined,
|
||||
'ignore_shopping': false,
|
||||
'supermarket_category': undefined,
|
||||
'target': {
|
||||
'id': undefined,
|
||||
'name': ''
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
search_input: _debounce(function() {
|
||||
this.left_page = 0
|
||||
this.foods = []
|
||||
this.left += 1
|
||||
}, 700),
|
||||
search_input2: _debounce(function() {
|
||||
this.right_page = 0
|
||||
this.foods2 = []
|
||||
this.right += 1
|
||||
}, 700)
|
||||
},
|
||||
methods: {
|
||||
resetSearch: function () {
|
||||
if (this.search_input !== '') {
|
||||
this.search_input = ''
|
||||
} else {
|
||||
this.left_page = 0
|
||||
this.foods = []
|
||||
this.left += 1
|
||||
}
|
||||
if (this.search_input2 !== '') {
|
||||
this.search_input2 = ''
|
||||
} else {
|
||||
this.right_page = 0
|
||||
// TODO should model actions be included with the context menu? the card? a seperate mixin avaible to all?
|
||||
resetList: function(e) {
|
||||
if (e.column === 'left') {
|
||||
this.foods = []
|
||||
} else if (e.column === 'right') {
|
||||
this.foods2 = []
|
||||
this.right += 1
|
||||
}
|
||||
},
|
||||
// TODO should model actions be included with the context menu? the card? a seperate mixin avaible to all?
|
||||
startAction: function(e, col) {
|
||||
let target = e?.target
|
||||
let source = e?.source
|
||||
if (e.action == 'delete') {
|
||||
this.this_item = source
|
||||
this.$bvModal.show('id_modal_food_delete')
|
||||
} else if (e.action == 'new') {
|
||||
this.this_item = {}
|
||||
this.$bvModal.show('id_modal_food_edit')
|
||||
} else if (e.action == 'edit') {
|
||||
this.this_item = source
|
||||
this.$bvModal.show('id_modal_food_edit')
|
||||
} else if (e.action === 'move') {
|
||||
this.this_item = source
|
||||
if (target == null) {
|
||||
this.$bvModal.show('id_modal_food_move')
|
||||
} else {
|
||||
this.moveFood(source.id, target.id)
|
||||
}
|
||||
} else if (e.action === 'merge') {
|
||||
this.this_item = source
|
||||
if (target == null) {
|
||||
this.$bvModal.show('id_modal_food_merge')
|
||||
} else {
|
||||
this.mergeFood(e.source.id, e.target.id)
|
||||
}
|
||||
} else if (e.action === 'get-children') {
|
||||
if (source.show_children) {
|
||||
Vue.set(source, 'show_children', false)
|
||||
} else {
|
||||
this.this_item = source
|
||||
this.getChildren(col, source)
|
||||
}
|
||||
} else if (e.action === 'get-recipes') {
|
||||
startAction: function(e, param) {
|
||||
let source = e?.source ?? this.blank_item
|
||||
let target = e?.target ?? undefined
|
||||
this.this_item = source
|
||||
this.this_item.target = target || undefined
|
||||
|
||||
if (source.show_recipes) {
|
||||
Vue.set(source, 'show_recipes', false)
|
||||
} else {
|
||||
this.this_item = source
|
||||
this.getRecipes(col, source)
|
||||
}
|
||||
switch (e.action) {
|
||||
case 'delete':
|
||||
this.$bvModal.show('id_modal_food_delete')
|
||||
break;
|
||||
case 'new':
|
||||
this.this_item = {...this.blank_item}
|
||||
this.$bvModal.show('id_modal_food_edit')
|
||||
break;
|
||||
case 'edit':
|
||||
this.$bvModal.show('id_modal_food_edit')
|
||||
break;
|
||||
case 'move':
|
||||
if (target == null) {
|
||||
this.$bvModal.show('id_modal_food_move')
|
||||
} else {
|
||||
this.moveFood(source.id, target.id)
|
||||
}
|
||||
break;
|
||||
case 'merge':
|
||||
if (target == null) {
|
||||
this.$bvModal.show('id_modal_food_merge')
|
||||
} else {
|
||||
this.mergeFood(e.source.id, e.target.id)
|
||||
}
|
||||
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;
|
||||
}
|
||||
},
|
||||
getFoods: function(params, callback) {
|
||||
let apiClient = new ApiApiFactory()
|
||||
let query = options?.query ?? ''
|
||||
let page = options?.page ?? 1
|
||||
let root = options?.root ?? undefined
|
||||
let tree = options?.tree ?? undefined
|
||||
let pageSize = options?.pageSize ?? 25
|
||||
|
||||
if (query === '') {
|
||||
query = undefined
|
||||
root = 0
|
||||
}
|
||||
// delete above
|
||||
let options = {
|
||||
'query': params?.query ?? '',
|
||||
'page': params?.page ?? 1,
|
||||
'root' : params?.id ?? undefined
|
||||
}
|
||||
let column = params?.column ?? 'left'
|
||||
|
||||
// let promise = this.listObjects(this.this_model, options).then(result => {
|
||||
let promise = apiClient.listFoods(query, root, tree, page, pageSize).then((result) => {
|
||||
if (result.data.results.length){
|
||||
if (column ==='left') {
|
||||
this.foods = this.foods.concat(result.data.results)
|
||||
if (this.foods?.length < result.data.count) {
|
||||
this.load_more_left = true
|
||||
} else {
|
||||
this.load_more_left = false
|
||||
}
|
||||
|
||||
} else if (column ==='right') {
|
||||
this.foods2 = this.foods2.concat(result.data.results)
|
||||
|
||||
if (this.foods2?.length < result.data.count) {
|
||||
this.load_more_right = true
|
||||
} else {
|
||||
this.load_more_right = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (column ==='left') {
|
||||
this.load_more_left = false
|
||||
} else if (column ==='right') {
|
||||
this.load_more_right = false
|
||||
}
|
||||
console.log('no data returned')
|
||||
}
|
||||
callback(promise)
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
|
||||
})
|
||||
},
|
||||
saveFood: function () {
|
||||
let apiClient = new ApiApiFactory()
|
||||
let food = {
|
||||
@ -358,47 +322,30 @@ export default {
|
||||
} else {
|
||||
this.foods2 = []
|
||||
}
|
||||
this.this_item={}
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
this.this_item = {}
|
||||
})
|
||||
} else {
|
||||
apiClient.partialUpdateFood(this.this_item.id, food).then(result => {
|
||||
this.refreshCard(this.this_item.id)
|
||||
this.this_item={}
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
this.this_item = {}
|
||||
})
|
||||
}
|
||||
},
|
||||
delFood: function (id) {
|
||||
let apiClient = new ApiApiFactory()
|
||||
apiClient.destroyFood(id).then(response => {
|
||||
this.destroyCard(id)
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
this.this_item = {}
|
||||
})
|
||||
this.this_item = {...this.blank_item}
|
||||
},
|
||||
moveFood: function (source_id, target_id) {
|
||||
let apiClient = new ApiApiFactory()
|
||||
apiClient.moveFood(String(source_id), String(target_id)).then(result => {
|
||||
if (target_id === 0) {
|
||||
let food = this.findFood(this.foods, source_id) || this.findFood(this.foods2, source_id)
|
||||
let food = this.findCard(this.foods, source_id) || this.findCard(this.foods2, source_id)
|
||||
food.parent = null
|
||||
if (this.show_split){
|
||||
this.destroyCard(source_id) // order matters, destroy old card before adding it back in at root
|
||||
this.foods = [food].concat(this.foods)
|
||||
this.foods2 = [JSON.parse(JSON.stringify(food))].concat(this.foods2)
|
||||
} else {
|
||||
this.destroyCard(source_id)
|
||||
this.foods = [food].concat(this.foods)
|
||||
this.foods2 = []
|
||||
}
|
||||
this.foods = [food].concat(this.destroyCard(source_id, this.foods))
|
||||
this.foods2 = [...food].concat(this.destroyCard(source_id, this.foods2)) // order matters, destroy old card before adding it back in at root
|
||||
|
||||
} else {
|
||||
this.destroyCard(source_id)
|
||||
this.foods = this.destroyCard(source_id, this.foods)
|
||||
this.foods2 = this.destroyCard(source_id, this.foods2)
|
||||
this.refreshCard(target_id)
|
||||
}
|
||||
}).catch((err) => {
|
||||
@ -411,7 +358,7 @@ export default {
|
||||
mergeFood: function (source_id, target_id) {
|
||||
let apiClient = new ApiApiFactory()
|
||||
apiClient.mergeFood(String(source_id), String(target_id)).then(result => {
|
||||
this.destroyCard(source_id)
|
||||
// this.destroyCard(source_id)
|
||||
this.refreshCard(target_id)
|
||||
}).catch((err) => {
|
||||
console.log('Error', err)
|
||||
@ -430,9 +377,9 @@ export default {
|
||||
|
||||
apiClient.listFoods(query, root, tree, page, pageSize).then(result => {
|
||||
if (col == 'left') {
|
||||
parent = this.findFood(this.foods, food.id)
|
||||
parent = this.findCard(this.foods, food.id)
|
||||
} else if (col == 'right'){
|
||||
parent = this.findFood(this.foods2, food.id)
|
||||
parent = this.findCard(this.foods2, food.id)
|
||||
}
|
||||
if (parent) {
|
||||
Vue.set(parent, 'children', result.data.results)
|
||||
@ -454,9 +401,9 @@ export default {
|
||||
undefined, undefined, undefined, undefined, undefined, pageSize, undefined
|
||||
).then(result => {
|
||||
if (col == 'left') {
|
||||
parent = this.findFood(this.foods, food.id)
|
||||
parent = this.findCard(this.foods, food.id)
|
||||
} else if (col == 'right'){
|
||||
parent = this.findFood(this.foods2, food.id)
|
||||
parent = this.findCard(this.foods2, food.id)
|
||||
}
|
||||
if (parent) {
|
||||
Vue.set(parent, 'recipes', result.data.results)
|
||||
@ -475,11 +422,11 @@ export default {
|
||||
let idx = undefined
|
||||
let idx2 = undefined
|
||||
apiClient.retrieveFood(id).then(result => {
|
||||
target = this.findFood(this.foods, id) || this.findFood(this.foods2, id)
|
||||
target = this.findCard(this.foods, id) || this.findCard(this.foods2, id)
|
||||
|
||||
if (target.parent) {
|
||||
let parent = this.findFood(this.foods, target.parent)
|
||||
let parent2 = this.findFood(this.foods2, target.parent)
|
||||
let parent = this.findCard(this.foods, target.parent)
|
||||
let parent2 = this.findCard(this.foods2, target.parent)
|
||||
|
||||
if (parent) {
|
||||
if (parent.show_children){
|
||||
@ -503,25 +450,6 @@ export default {
|
||||
|
||||
})
|
||||
},
|
||||
findFood: function(food_list, id){
|
||||
if (food_list.length == 0) {
|
||||
return false
|
||||
}
|
||||
let food = food_list.filter(fd => fd.id == id)
|
||||
if (food.length == 1) {
|
||||
return food[0]
|
||||
} else if (food.length == 0) {
|
||||
for (const f of food_list.filter(fd => fd.show_children == true)) {
|
||||
food = this.findFood(f.children, id)
|
||||
if (food) {
|
||||
return food
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
console.log('something terrible happened')
|
||||
}
|
||||
},
|
||||
// this would move with modals with mixin?
|
||||
prepareEmoji: function() {
|
||||
this.$refs._edit.addText(this.this_item.icon || '');
|
||||
@ -532,73 +460,15 @@ export default {
|
||||
setIcon: function(icon) {
|
||||
this.this_item.icon = icon
|
||||
},
|
||||
infiniteHandler: function($state, col) {
|
||||
let apiClient = new ApiApiFactory()
|
||||
let query = (col==='left') ? this.search_input : this.search_input2
|
||||
let page = (col==='left') ? this.left_page + 1 : this.right_page + 1
|
||||
let root = undefined
|
||||
let tree = undefined
|
||||
let pageSize = undefined
|
||||
|
||||
if (query === '') {
|
||||
query = undefined
|
||||
root = 0
|
||||
}
|
||||
|
||||
apiClient.listFoods(query, root, tree, page, pageSize).then(result => {
|
||||
if (result.data.results.length){
|
||||
if (col ==='left') {
|
||||
this.left_page+=1
|
||||
this.foods = this.foods.concat(result.data.results)
|
||||
$state.loaded();
|
||||
if (this.foods.length >= result.data.count) {
|
||||
$state.complete();
|
||||
}
|
||||
} else if (col ==='right') {
|
||||
this.right_page+=1
|
||||
this.foods2 = this.foods2.concat(result.data.results)
|
||||
$state.loaded();
|
||||
if (this.foods2.length >= result.data.count) {
|
||||
$state.complete();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('no data returned')
|
||||
$state.complete();
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
|
||||
$state.complete();
|
||||
deleteThis: function(id, model) {
|
||||
const result = new Promise((callback) => this.deleteObject(id, model, callback))
|
||||
result.then(() => {
|
||||
this.foods = this.destroyCard(id, this.foods)
|
||||
this.foods2 = this.destroyCard(id, this.foods2)
|
||||
})
|
||||
},
|
||||
destroyCard: function(id) {
|
||||
let fd = this.findFood(this.foods, id)
|
||||
let fd2 = this.findFood(this.foods2, id)
|
||||
let p_id = undefined
|
||||
p_id = fd?.parent ?? fd2.parent
|
||||
|
||||
if (p_id) {
|
||||
let parent = this.findFood(this.foods, p_id)
|
||||
let parent2 = this.findFood(this.foods2, p_id)
|
||||
if (parent){
|
||||
Vue.set(parent, 'numchild', parent.numchild - 1)
|
||||
if (parent.show_children) {
|
||||
let idx = parent.children.indexOf(parent.children.find(kw => kw.id === id))
|
||||
Vue.delete(parent.children, idx)
|
||||
}
|
||||
}
|
||||
if (parent2){
|
||||
Vue.set(parent2, 'numchild', parent2.numchild - 1)
|
||||
if (parent2.show_children) {
|
||||
let idx = parent2.children.indexOf(parent2.children.find(kw => kw.id === id))
|
||||
Vue.delete(parent2.children, idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
this.foods = this.foods.filter(kw => kw.id != id)
|
||||
this.foods2 = this.foods2.filter(kw => kw.id != id)
|
||||
},
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
611
vue/src/apps/FoodListView/FoodListView_orig.vue
Normal file
611
vue/src/apps/FoodListView/FoodListView_orig.vue
Normal file
@ -0,0 +1,611 @@
|
||||
<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="startAction({'action':'new'})">
|
||||
{{ this.$t('New_Food') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3" style="margin-top: 1vh">
|
||||
<button class="btn btn-primary btn-block text-uppercase" @click="resetSearch">
|
||||
{{ this.$t('Reset_Search') }}
|
||||
</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.$t('show_split_screen') }}
|
||||
</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_input"
|
||||
v-bind:placeholder="this.$t('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_input2"
|
||||
v-bind:placeholder="this.$t('Search')"></b-input>
|
||||
</b-input-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- only show scollbars in split mode, but this doesn't interact well with infinite scroll, maybe a different componenet? -->
|
||||
<div class="row" :class="{'overflow-hidden' : show_split}" style="margin-top: 2vh">
|
||||
<div class="col col-md" :class="{'mh-100 overflow-auto' : show_split}">
|
||||
<generic-horizontal-card v-for="f in foods" v-bind:key="f.id"
|
||||
:model=f
|
||||
model_name="Food"
|
||||
:draggable="true"
|
||||
:merge="true"
|
||||
:move="true"
|
||||
@item-action="startAction($event, 'left')"
|
||||
>
|
||||
<template v-slot:upper-right>
|
||||
<b-button v-if="f.recipe" v-b-tooltip.hover :title="f.recipe.name"
|
||||
class=" btn fas fa-book-open p-0 border-0" variant="link" :href="f.recipe.url"/>
|
||||
</template>
|
||||
</generic-horizontal-card>
|
||||
<infinite-loading
|
||||
:identifier='left'
|
||||
@infinite="infiniteHandler($event, 'left')"
|
||||
spinner="waveDots">
|
||||
<template v-slot:no-more><span/></template>
|
||||
</infinite-loading>
|
||||
</div>
|
||||
<!-- right side food cards -->
|
||||
<div class="col col-md mh-100 overflow-auto " v-if="show_split">
|
||||
<generic-horizontal-card v-for="f in foods" v-bind:key="f.id"
|
||||
:model=f
|
||||
model_name="Food"
|
||||
:draggable="true"
|
||||
:merge="true"
|
||||
:move="true"
|
||||
@item-action="startAction($event, 'left')"
|
||||
>
|
||||
<template v-slot:upper-right>
|
||||
<b-button v-if="f.recipe" v-b-tooltip.hover :title="f.recipe.name"
|
||||
class=" btn fas fa-book-open p-0 border-0" variant="link" :href="f.recipe.url"/>
|
||||
</template>
|
||||
</generic-horizontal-card>
|
||||
<infinite-loading
|
||||
:identifier='right'
|
||||
@infinite="infiniteHandler($event, 'right')"
|
||||
spinner="waveDots">
|
||||
<template v-slot:no-more><span/></template>
|
||||
</infinite-loading>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="col-md-2 d-none d-md-block">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TODO Modals can probably be made generic and moved to component -->
|
||||
<!-- edit modal -->
|
||||
<b-modal class="modal"
|
||||
:id="'id_modal_food_edit'"
|
||||
:title="this.$t('Edit_Food')"
|
||||
:ok-title="this.$t('Save')"
|
||||
:cancel-title="this.$t('Cancel')"
|
||||
@ok="saveFood">
|
||||
<form>
|
||||
<label for="id_food_name_edit">{{ this.$t('Name') }}</label>
|
||||
<input class="form-control" type="text" id="id_food_name_edit" v-model="this_item.name">
|
||||
<label for="id_food_description_edit">{{ this.$t('Description') }}</label>
|
||||
<input class="form-control" type="text" id="id_food_description_edit" v-model="this_item.description">
|
||||
<label for="id_food_recipe_edit">{{ this.$t('Recipe') }}</label>
|
||||
<!-- TODO initial selection isn't working and I don't know why -->
|
||||
<generic-multiselect
|
||||
@change="this_item.recipe=$event.val"
|
||||
label="name"
|
||||
:initial_selection="[this_item.recipe]"
|
||||
search_function="listRecipes"
|
||||
:multiple="false"
|
||||
:sticky_options="[{'id': null,'name': $t('None')}]"
|
||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||
:placeholder="this.$t('Search')">
|
||||
</generic-multiselect>
|
||||
<div class="form-group form-check">
|
||||
<input type="checkbox" class="form-check-input" id="id_food_ignore_edit" v-model="this_item.ignore_shopping">
|
||||
<label class="form-check-label" for="id_food_ignore_edit">{{ this.$t('Ignore_Shopping') }}</label>
|
||||
</div>
|
||||
<label for="id_food_category_edit">{{ this.$t('Shopping_Category') }}</label>
|
||||
<generic-multiselect
|
||||
@change="this_item.supermarket_category=$event.val"
|
||||
label="name"
|
||||
:initial_selection="[this_item.supermarket_category]"
|
||||
search_function="listSupermarketCategorys"
|
||||
:multiple="false"
|
||||
:sticky_options="[{'id': null,'name': $t('None')}]"
|
||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||
:placeholder="this.$t('Shopping_Category')">
|
||||
</generic-multiselect>
|
||||
</form>
|
||||
</b-modal>
|
||||
<!-- delete modal -->
|
||||
<b-modal class="modal"
|
||||
:id="'id_modal_food_delete'"
|
||||
:title="this.$t('Delete_Food')"
|
||||
:ok-title="this.$t('Delete')"
|
||||
:cancel-title="this.$t('Cancel')"
|
||||
@ok="delFood(this_item.id)">
|
||||
{{this.$t("delete_confimation", {'kw': this_item.name})}} {{this_item.name}}
|
||||
</b-modal>
|
||||
<!-- move modal -->
|
||||
<b-modal class="modal"
|
||||
:id="'id_modal_food_move'"
|
||||
:title="this.$t('Move_Food')"
|
||||
:ok-title="this.$t('Move')"
|
||||
:cancel-title="this.$t('Cancel')"
|
||||
@ok="moveFood(this_item.id, this_item.target.id)">
|
||||
{{ this.$t("move_selection", {'child': this_item.name}) }}
|
||||
<generic-multiselect
|
||||
@change="this_item.target=$event.val"
|
||||
label="name"
|
||||
search_function="listFoods"
|
||||
:multiple="false"
|
||||
:sticky_options="[{'id': 0,'name': $t('Root')}]"
|
||||
:tree_api="true"
|
||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||
:placeholder="this.$t('Search')">
|
||||
</generic-multiselect>
|
||||
</b-modal>
|
||||
<!-- merge modal -->
|
||||
<b-modal class="modal"
|
||||
:id="'id_modal_food_merge'"
|
||||
:title="this.$t('Merge_Food')"
|
||||
:ok-title="this.$t('Merge')"
|
||||
:cancel-title="this.$t('Cancel')"
|
||||
@ok="mergeFood(this_item.id, this_item.target.id)">
|
||||
{{ this.$t("merge_selection", {'source': this_item.name, 'type': this.$t('food')}) }}
|
||||
<generic-multiselect
|
||||
@change="this_item.target=$event.val"
|
||||
label="name"
|
||||
search_function="listFoods"
|
||||
:multiple="false"
|
||||
:tree_api="true"
|
||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||
:placeholder="this.$t('Search')">
|
||||
</generic-multiselect>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import axios from "axios";
|
||||
axios.defaults.xsrfCookieName = 'csrftoken'
|
||||
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
|
||||
|
||||
import Vue from 'vue'
|
||||
import {BootstrapVue} from 'bootstrap-vue'
|
||||
|
||||
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
||||
import _debounce from 'lodash/debounce'
|
||||
|
||||
import {ToastMixin} from "@/utils/utils";
|
||||
|
||||
import {ApiApiFactory} from "@/utils/openapi/api.ts";
|
||||
// import FoodCard from "@/components/FoodCard";
|
||||
import GenericHorizontalCard from "@/components/GenericHorizontalCard";
|
||||
import GenericMultiselect from "@/components/GenericMultiselect";
|
||||
import InfiniteLoading from 'vue-infinite-loading';
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
export default {
|
||||
name: 'FoodListView',
|
||||
mixins: [ToastMixin],
|
||||
components: {GenericHorizontalCard, GenericMultiselect, InfiniteLoading},
|
||||
data() {
|
||||
return {
|
||||
foods: [],
|
||||
foods2: [],
|
||||
show_split: false,
|
||||
search_input: '',
|
||||
search_input2: '',
|
||||
advanced_visible: false,
|
||||
right_page: 0,
|
||||
right: +new Date(),
|
||||
isDirtyRight: false,
|
||||
left_page: 0,
|
||||
update_recipe: [],
|
||||
left: +new Date(),
|
||||
isDirtyLeft: false,
|
||||
this_item: {
|
||||
'id': -1,
|
||||
'name': '',
|
||||
'description': '',
|
||||
'recipe': null,
|
||||
'recipe_full': undefined,
|
||||
'ignore_shopping': false,
|
||||
'supermarket_category': undefined,
|
||||
'target': {
|
||||
'id': -1,
|
||||
'name': ''
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
search_input: _debounce(function() {
|
||||
this.left_page = 0
|
||||
this.foods = []
|
||||
this.left += 1
|
||||
}, 700),
|
||||
search_input2: _debounce(function() {
|
||||
this.right_page = 0
|
||||
this.foods2 = []
|
||||
this.right += 1
|
||||
}, 700)
|
||||
},
|
||||
methods: {
|
||||
resetSearch: function () {
|
||||
if (this.search_input !== '') {
|
||||
this.search_input = ''
|
||||
} else {
|
||||
this.left_page = 0
|
||||
this.foods = []
|
||||
this.left += 1
|
||||
}
|
||||
if (this.search_input2 !== '') {
|
||||
this.search_input2 = ''
|
||||
} else {
|
||||
this.right_page = 0
|
||||
this.foods2 = []
|
||||
this.right += 1
|
||||
}
|
||||
},
|
||||
// TODO should model actions be included with the context menu? the card? a seperate mixin avaible to all?
|
||||
startAction: function(e, col) {
|
||||
let target = e?.target
|
||||
let source = e?.source
|
||||
if (e.action == 'delete') {
|
||||
this.this_item = source
|
||||
this.$bvModal.show('id_modal_food_delete')
|
||||
} else if (e.action == 'new') {
|
||||
this.this_item = {}
|
||||
this.$bvModal.show('id_modal_food_edit')
|
||||
} else if (e.action == 'edit') {
|
||||
this.this_item = source
|
||||
this.$bvModal.show('id_modal_food_edit')
|
||||
} else if (e.action === 'move') {
|
||||
this.this_item = source
|
||||
if (target == null) {
|
||||
this.$bvModal.show('id_modal_food_move')
|
||||
} else {
|
||||
this.moveFood(source.id, target.id)
|
||||
}
|
||||
} else if (e.action === 'merge') {
|
||||
this.this_item = source
|
||||
if (target == null) {
|
||||
this.$bvModal.show('id_modal_food_merge')
|
||||
} else {
|
||||
this.mergeFood(e.source.id, e.target.id)
|
||||
}
|
||||
} else if (e.action === 'get-children') {
|
||||
if (source.show_children) {
|
||||
Vue.set(source, 'show_children', false)
|
||||
} else {
|
||||
this.this_item = source
|
||||
this.getChildren(col, source)
|
||||
}
|
||||
} else if (e.action === 'get-recipes') {
|
||||
|
||||
if (source.show_recipes) {
|
||||
Vue.set(source, 'show_recipes', false)
|
||||
} else {
|
||||
this.this_item = source
|
||||
this.getRecipes(col, source)
|
||||
}
|
||||
}
|
||||
},
|
||||
saveFood: function () {
|
||||
let apiClient = new ApiApiFactory()
|
||||
let food = {
|
||||
name: this.this_item.name,
|
||||
description: this.this_item.description,
|
||||
recipe: this.this_item.recipe?.id ?? null,
|
||||
ignore_shopping: this.this_item.ignore_shopping,
|
||||
supermarket_category: this.this_item.supermarket_category?.id ?? null,
|
||||
}
|
||||
if (!this.this_item.id) { // if there is no item id assume its a new item
|
||||
apiClient.createFood(food).then(result => {
|
||||
// place all new foods at the top of the list - could sort instead
|
||||
this.foods = [result.data].concat(this.foods)
|
||||
// this creates a deep copy to make sure that columns stay independent
|
||||
if (this.show_split){
|
||||
this.foods2 = [JSON.parse(JSON.stringify(result.data))].concat(this.foods2)
|
||||
} else {
|
||||
this.foods2 = []
|
||||
}
|
||||
this.this_item={}
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
this.this_item = {}
|
||||
})
|
||||
} else {
|
||||
apiClient.partialUpdateFood(this.this_item.id, food).then(result => {
|
||||
this.refreshCard(this.this_item.id)
|
||||
this.this_item={}
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
this.this_item = {}
|
||||
})
|
||||
}
|
||||
},
|
||||
delFood: function (id) {
|
||||
let apiClient = new ApiApiFactory()
|
||||
apiClient.destroyFood(id).then(response => {
|
||||
this.destroyCard(id)
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
this.this_item = {}
|
||||
})
|
||||
},
|
||||
moveFood: function (source_id, target_id) {
|
||||
let apiClient = new ApiApiFactory()
|
||||
apiClient.moveFood(String(source_id), String(target_id)).then(result => {
|
||||
if (target_id === 0) {
|
||||
let food = this.findFood(this.foods, source_id) || this.findFood(this.foods2, source_id)
|
||||
food.parent = null
|
||||
if (this.show_split){
|
||||
this.destroyCard(source_id) // order matters, destroy old card before adding it back in at root
|
||||
this.foods = [food].concat(this.foods)
|
||||
this.foods2 = [JSON.parse(JSON.stringify(food))].concat(this.foods2)
|
||||
} else {
|
||||
this.destroyCard(source_id)
|
||||
this.foods = [food].concat(this.foods)
|
||||
this.foods2 = []
|
||||
}
|
||||
} else {
|
||||
this.destroyCard(source_id)
|
||||
this.refreshCard(target_id)
|
||||
}
|
||||
}).catch((err) => {
|
||||
// TODO none of the error checking works because the openapi generated functions don't throw an error?
|
||||
// or i'm capturing it incorrectly
|
||||
console.log(err)
|
||||
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
|
||||
})
|
||||
},
|
||||
mergeFood: function (source_id, target_id) {
|
||||
let apiClient = new ApiApiFactory()
|
||||
apiClient.mergeFood(String(source_id), String(target_id)).then(result => {
|
||||
this.destroyCard(source_id)
|
||||
this.refreshCard(target_id)
|
||||
}).catch((err) => {
|
||||
console.log('Error', err)
|
||||
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
|
||||
})
|
||||
},
|
||||
// TODO: DRY the listFood functions (refresh, get children, infinityHandler ) can probably all be consolidated into a single function
|
||||
getChildren: function(col, food){
|
||||
let apiClient = new ApiApiFactory()
|
||||
let parent = {}
|
||||
let query = undefined
|
||||
let page = undefined
|
||||
let root = food.id
|
||||
let tree = undefined
|
||||
let pageSize = 200
|
||||
|
||||
apiClient.listFoods(query, root, tree, page, pageSize).then(result => {
|
||||
if (col == 'left') {
|
||||
parent = this.findFood(this.foods, food.id)
|
||||
} else if (col == 'right'){
|
||||
parent = this.findFood(this.foods2, food.id)
|
||||
}
|
||||
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, food){
|
||||
let apiClient = new ApiApiFactory()
|
||||
let parent = {}
|
||||
let pageSize = 200
|
||||
|
||||
apiClient.listRecipes(
|
||||
undefined, undefined, String(food.id), undefined, undefined, undefined,
|
||||
undefined, undefined, undefined, undefined, undefined, pageSize, undefined
|
||||
).then(result => {
|
||||
if (col == 'left') {
|
||||
parent = this.findFood(this.foods, food.id)
|
||||
} else if (col == 'right'){
|
||||
parent = this.findFood(this.foods2, food.id)
|
||||
}
|
||||
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')
|
||||
})
|
||||
},
|
||||
refreshCard: function(id){
|
||||
let target = {}
|
||||
let apiClient = new ApiApiFactory()
|
||||
let idx = undefined
|
||||
let idx2 = undefined
|
||||
apiClient.retrieveFood(id).then(result => {
|
||||
target = this.findFood(this.foods, id) || this.findFood(this.foods2, id)
|
||||
|
||||
if (target.parent) {
|
||||
let parent = this.findFood(this.foods, target.parent)
|
||||
let parent2 = this.findFood(this.foods2, target.parent)
|
||||
|
||||
if (parent) {
|
||||
if (parent.show_children){
|
||||
idx = parent.children.indexOf(parent.children.find(kw => kw.id === target.id))
|
||||
Vue.set(parent.children, idx, result.data)
|
||||
}
|
||||
}
|
||||
if (parent2){
|
||||
if (parent2.show_children){
|
||||
idx2 = parent2.children.indexOf(parent2.children.find(kw => kw.id === target.id))
|
||||
// deep copy to force columns to be indepedent
|
||||
Vue.set(parent2.children, idx2, JSON.parse(JSON.stringify(result.data)))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
idx = this.foods.indexOf(this.foods.find(food => food.id === target.id))
|
||||
idx2 = this.foods2.indexOf(this.foods2.find(food => food.id === target.id))
|
||||
Vue.set(this.foods, idx, result.data)
|
||||
Vue.set(this.foods2, idx2, JSON.parse(JSON.stringify(result.data)))
|
||||
}
|
||||
|
||||
})
|
||||
},
|
||||
findFood: function(food_list, id){
|
||||
if (food_list.length == 0) {
|
||||
return false
|
||||
}
|
||||
let food = food_list.filter(fd => fd.id == id)
|
||||
if (food.length == 1) {
|
||||
return food[0]
|
||||
} else if (food.length == 0) {
|
||||
for (const f of food_list.filter(fd => fd.show_children == true)) {
|
||||
food = this.findFood(f.children, id)
|
||||
if (food) {
|
||||
return food
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
console.log('something terrible happened')
|
||||
}
|
||||
},
|
||||
// this would move with modals with mixin?
|
||||
prepareEmoji: function() {
|
||||
this.$refs._edit.addText(this.this_item.icon || '');
|
||||
this.$refs._edit.blur()
|
||||
document.getElementById('btn-emoji-default').disabled = true;
|
||||
},
|
||||
// this would move with modals with mixin?
|
||||
setIcon: function(icon) {
|
||||
this.this_item.icon = icon
|
||||
},
|
||||
infiniteHandler: function($state, col) {
|
||||
let apiClient = new ApiApiFactory()
|
||||
let query = (col==='left') ? this.search_input : this.search_input2
|
||||
let page = (col==='left') ? this.left_page + 1 : this.right_page + 1
|
||||
let root = undefined
|
||||
let tree = undefined
|
||||
let pageSize = undefined
|
||||
|
||||
if (query === '') {
|
||||
query = undefined
|
||||
root = 0
|
||||
}
|
||||
|
||||
apiClient.listFoods(query, root, tree, page, pageSize).then(result => {
|
||||
if (result.data.results.length){
|
||||
if (col ==='left') {
|
||||
this.left_page+=1
|
||||
this.foods = this.foods.concat(result.data.results)
|
||||
$state.loaded();
|
||||
if (this.foods.length >= result.data.count) {
|
||||
$state.complete();
|
||||
}
|
||||
} else if (col ==='right') {
|
||||
this.right_page+=1
|
||||
this.foods2 = this.foods2.concat(result.data.results)
|
||||
$state.loaded();
|
||||
if (this.foods2.length >= result.data.count) {
|
||||
$state.complete();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('no data returned')
|
||||
$state.complete();
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
|
||||
$state.complete();
|
||||
})
|
||||
},
|
||||
destroyCard: function(id) {
|
||||
let fd = this.findFood(this.foods, id)
|
||||
let fd2 = this.findFood(this.foods2, id)
|
||||
let p_id = undefined
|
||||
p_id = fd?.parent ?? fd2.parent
|
||||
|
||||
if (p_id) {
|
||||
let parent = this.findFood(this.foods, p_id)
|
||||
let parent2 = this.findFood(this.foods2, p_id)
|
||||
if (parent){
|
||||
Vue.set(parent, 'numchild', parent.numchild - 1)
|
||||
if (parent.show_children) {
|
||||
let idx = parent.children.indexOf(parent.children.find(kw => kw.id === id))
|
||||
Vue.delete(parent.children, idx)
|
||||
}
|
||||
}
|
||||
if (parent2){
|
||||
Vue.set(parent2, 'numchild', parent2.numchild - 1)
|
||||
if (parent2.show_children) {
|
||||
let idx = parent2.children.indexOf(parent2.children.find(kw => kw.id === id))
|
||||
Vue.delete(parent2.children, idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
this.foods = this.foods.filter(kw => kw.id != id)
|
||||
this.foods2 = this.foods2.filter(kw => kw.id != id)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
@ -11,7 +11,7 @@
|
||||
@drop="handleDragDrop($event)">
|
||||
<b-row no-gutters style="height:inherit;">
|
||||
<b-col no-gutters md="3" style="height:inherit;">
|
||||
<b-card-img-lazy style="object-fit: cover; height: 10vh;" :src="model_image" v-bind:alt="text.image_alt"></b-card-img-lazy>
|
||||
<b-card-img-lazy style="object-fit: cover; height: 10vh;" :src="model_image" v-bind:alt="$t('Recipe_Image')"></b-card-img-lazy>
|
||||
</b-col>
|
||||
<b-col no-gutters md="9" style="height:inherit;">
|
||||
<b-card-body class="m-0 py-0" style="height:inherit;">
|
||||
@ -35,7 +35,6 @@
|
||||
</b-col>
|
||||
<div class="card-img-overlay justify-content-right h-25 m-0 p-0 text-right">
|
||||
<slot name="upper-right"></slot>
|
||||
|
||||
<generic-context-menu class="p-0"
|
||||
:show_merge="merge"
|
||||
:show_move="move"
|
||||
@ -74,7 +73,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- this should be made a generic component, would also require mixin for functions that generate the popup and put in parent container-->
|
||||
<!-- this should be made a generic component, would also require mixin for functions that generate the popup and put in parent container-->
|
||||
<b-list-group ref="tooltip" variant="light" v-show="show_menu" v-on-clickaway="closeMenu" style="z-index:999; cursor:pointer">
|
||||
<b-list-group-item v-if="move" action v-on:click="$emit('item-action',{'action': 'move', 'target': model, 'source': source}); closeMenu()">
|
||||
{{$t('Move')}}: {{$t('move_confirmation', {'child': source.name,'parent':model.name})}}
|
||||
@ -92,6 +91,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {ApiApiFactory} from "@/utils/openapi/api.ts";
|
||||
import GenericContextMenu from "@/components/GenericContextMenu";
|
||||
import RecipeCard from "@/components/RecipeCard";
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
@ -113,6 +113,7 @@ export default {
|
||||
recipes: {type: String, default: 'recipes'},
|
||||
merge: {type: Boolean, default: false},
|
||||
move: {type: Boolean, default: false},
|
||||
tree: {type: Boolean, default: false},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -124,7 +125,6 @@ export default {
|
||||
source: {'id': undefined, 'name': undefined},
|
||||
target: {'id': undefined, 'name': undefined},
|
||||
text: {
|
||||
'image_alt': '',
|
||||
'hide_children': '',
|
||||
}
|
||||
}
|
||||
@ -132,8 +132,7 @@ export default {
|
||||
mounted() {
|
||||
this.model_image = this.model?.image ?? window.IMAGE_PLACEHOLDER
|
||||
this.dragMenu = this.$refs.tooltip
|
||||
this.text.image_alt = this.$t(this.model_name + '_Image'),
|
||||
this.hide_children = this.$t('Hide_' + this.model_name)
|
||||
this.text.hide_children = this.$t('Hide_' + this.model_name)
|
||||
},
|
||||
methods: {
|
||||
emitAction: function(m) {
|
||||
@ -199,7 +198,39 @@ export default {
|
||||
},
|
||||
closeMenu: function(){
|
||||
this.show_menu = false
|
||||
},
|
||||
deleteObject: function(id, model, callback) {
|
||||
let apiClient = new ApiApiFactory()
|
||||
let promise = apiClient['destroy' + model](id).then(() => {
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
|
||||
})
|
||||
callback(promise)
|
||||
},
|
||||
async listObjects(model, options) {
|
||||
let apiClient = new ApiApiFactory()
|
||||
let query = options?.query ?? ''
|
||||
let page = options?.page ?? 1
|
||||
let root = options?.root ?? undefined
|
||||
let tree = options?.tree ?? undefined
|
||||
let pageSize = options?.pageSize ?? 25
|
||||
|
||||
if (this.tree) {
|
||||
if (query === '') {
|
||||
query = undefined
|
||||
root = 0
|
||||
}
|
||||
await apiClient.listFoods(query, root, tree, page, pageSize).then((result) => {
|
||||
return result
|
||||
})
|
||||
} else {
|
||||
await apiClient.listFoods(query, page, pageSize).then((result) => {
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -66,7 +66,7 @@ export default {
|
||||
let page = 1
|
||||
let root = undefined
|
||||
let tree = undefined
|
||||
let pageSize = 25
|
||||
let pageSize = 10
|
||||
|
||||
if (query === '') {
|
||||
query = undefined
|
||||
|
235
vue/src/components/GenericSplitLists.vue
Normal file
235
vue/src/components/GenericSplitLists.vue
Normal file
@ -0,0 +1,235 @@
|
||||
<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_right"
|
||||
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_left"
|
||||
v-bind:placeholder="this.text.search"></b-input>
|
||||
</b-input-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- only show scollbars in split mode -->
|
||||
<!-- 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>
|
||||
</infinite-loading>
|
||||
</div>
|
||||
<!-- right side food 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>
|
||||
</infinite-loading>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 d-none d-md-block">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue' // maybe not needed?
|
||||
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
||||
import _debounce from 'lodash/debounce'
|
||||
import InfiniteLoading from 'vue-infinite-loading';
|
||||
|
||||
export default {
|
||||
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 []}},
|
||||
right_list: {type:Array, default(){return []}},
|
||||
load_more_left: {type: Boolean, default: false},
|
||||
load_more_right: {type: Boolean, default: false},
|
||||
//merge: {type: Boolean, default: false},
|
||||
//move: {type: Boolean, default: false},
|
||||
//action: Object
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
advanced_visible: false,
|
||||
show_split: false,
|
||||
search_right: '',
|
||||
search_left: '',
|
||||
right_page: 1,
|
||||
left_page: 1,
|
||||
right: +new Date(),
|
||||
left: +new Date(),
|
||||
isDirtyright: false,
|
||||
isDirtyleft: false,
|
||||
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)
|
||||
this.text.name = this.$t(this.list_name)
|
||||
},
|
||||
watch: {
|
||||
search_right: _debounce(function() {
|
||||
this.left_page = 0
|
||||
this.$emit('reset', {'column':'left'})
|
||||
this.left += 1
|
||||
}, 700),
|
||||
search_left: _debounce(function() {
|
||||
this.right_page = 0
|
||||
this.$emit('reset', {'column':'right'})
|
||||
this.right += 1
|
||||
}, 700),
|
||||
},
|
||||
methods: {
|
||||
resetSearch: function () {
|
||||
if (this.search_right !== '') {
|
||||
this.search_right = ''
|
||||
} else {
|
||||
this.left_page = 1
|
||||
this.$emit('reset', {'column':'left'})
|
||||
this.left += 1
|
||||
}
|
||||
if (this.search_left !== '') {
|
||||
this.search_left = ''
|
||||
} else {
|
||||
this.right_page = 1
|
||||
this.$emit('reset', {'column':'right'})
|
||||
this.right += 1
|
||||
}
|
||||
},
|
||||
infiniteHandler: function($state, col) {
|
||||
let params = {
|
||||
'query': (col==='left') ? this.search_right : this.search_left,
|
||||
'page': (col==='left') ? this.left_page + 1 : this.right_page + 1,
|
||||
'column': col
|
||||
}
|
||||
const result = new Promise((callback) => this.$emit('get-list', params, callback))
|
||||
result.then((result) => {
|
||||
console.log(result)
|
||||
this[col+'_page']+=1
|
||||
$state.loaded();
|
||||
if (!this['load_more_' + col]) {
|
||||
$state.complete();
|
||||
}
|
||||
}).catch(() => {
|
||||
$state.complete();
|
||||
})
|
||||
},
|
||||
findCard: function(card_list, id){
|
||||
let card_length = card_list?.length ?? 0
|
||||
if (card_length == 0) {
|
||||
return false
|
||||
}
|
||||
let cards = card_list.filter(obj => obj.id == id)
|
||||
if (cards.length == 1) {
|
||||
return cards[0]
|
||||
} else if (cards.length == 0) {
|
||||
for (const o of card_list.filter(o => o.show_children == true)) {
|
||||
cards = this.findCard(o.children, id)
|
||||
if (cards) {
|
||||
return cards
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('something terrible happened')
|
||||
}
|
||||
},
|
||||
destroyCard: function(id, cardList) {
|
||||
let card = this.findCard(cardList, id)
|
||||
let card_id = undefined
|
||||
let p_id = card?.parent ?? undefined
|
||||
|
||||
if (p_id) {
|
||||
let parent = this.findCard(cardList, p_id)
|
||||
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))
|
||||
Vue.delete(parent.children, idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
return cardList.filter(kw => kw.id != id)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
@ -108,5 +108,7 @@
|
||||
"Edit_Food": "Edit Food",
|
||||
"Move_Food": "Move Food",
|
||||
"New_Food": "New Food",
|
||||
"Hide_Foods": "Hide Food"
|
||||
"Hide_Food": "Hide Food",
|
||||
"Delete_Food": "Delete Food",
|
||||
"No_ID": "ID not found, cannot delete."
|
||||
}
|
||||
|
10250
vue/yarn.lock
10250
vue/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user