working WIP

This commit is contained in:
smilerz 2021-08-23 20:50:54 -05:00
parent d606ea8db3
commit f7b2af7f97
20 changed files with 2201 additions and 17460 deletions

View File

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

View File

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

7961
vue/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -22,6 +22,7 @@
"vue-cookies": "^1.7.4", "vue-cookies": "^1.7.4",
"vue-i18n": "^8.24.4", "vue-i18n": "^8.24.4",
"vue-infinite-loading": "^2.4.5", "vue-infinite-loading": "^2.4.5",
"vue-infinite-scroll": "^2.0.2",
"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",

View File

@ -1,76 +1,20 @@
<template> <template>
<div id="app" style="margin-bottom: 4vh"> <div id="app" style="margin-bottom: 4vh">
<generic-split-lists
<div class="row"> :list_name="this_model"
<div class="col-md-2 d-none d-md-block"> :load_more_left="load_more_left"
:load_more_right="load_more_right"
</div> @reset="resetList"
<div class="col-xl-8 col-12"> @get-list="getFoods"
<div class="container-fluid d-flex flex-column flex-grow-1" :class="{'vh-100' : show_split}"> @item-action="startAction"
<!-- expanded options box --> >
<div class="row flex-shrink-0"> <template v-slot:cards-left>
<div class="col col-md-12"> <generic-horizontal-card
<b-collapse id="collapse_advanced" class="mt-2" v-model="advanced_visible"> v-for="f in foods" v-bind:key="f.id"
<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=f
model_name="Food" :model_name="this_model"
:draggable="true" :draggable="true"
:tree="true"
:merge="true" :merge="true"
:move="true" :move="true"
@item-action="startAction($event, 'left')" @item-action="startAction($event, 'left')"
@ -80,44 +24,24 @@
class=" btn fas fa-book-open p-0 border-0" variant="link" :href="f.recipe.url"/> class=" btn fas fa-book-open p-0 border-0" variant="link" :href="f.recipe.url"/>
</template> </template>
</generic-horizontal-card> </generic-horizontal-card>
<infinite-loading </template>
:identifier='left' <template v-slot:cards-right>
@infinite="infiniteHandler($event, 'left')" <generic-horizontal-card v-for="f in foods2" v-bind:key="f.id"
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=f
model_name="Food" :model_name="this_model"
:draggable="true" :draggable="true"
:tree="true"
:merge="true" :merge="true"
:move="true" :move="true"
@item-action="startAction($event, 'left')" @item-action="startAction($event, 'right')"
> >
<template v-slot:upper-right> <template v-slot:upper-right>
<b-button v-if="f.recipe" v-b-tooltip.hover :title="f.recipe.name" <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"/> class=" btn fas fa-book-open p-0 border-0" variant="link" :href="f.recipe.url"/>
</template> </template>
</generic-horizontal-card> </generic-horizontal-card>
<infinite-loading </template>
:identifier='right' </generic-split-lists>
@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 --> <!-- TODO Modals can probably be made generic and moved to component -->
<!-- edit modal --> <!-- edit modal -->
@ -167,8 +91,8 @@
:title="this.$t('Delete_Food')" :title="this.$t('Delete_Food')"
:ok-title="this.$t('Delete')" :ok-title="this.$t('Delete')"
:cancel-title="this.$t('Cancel')" :cancel-title="this.$t('Cancel')"
@ok="delFood(this_item.id)"> @ok="deleteThis(this_item.id, this_model)">
{{this.$t("delete_confimation", {'kw': this_item.name})}} {{this_item.name}} {{this.$t("delete_confimation", {'kw': this_item.name})}}
</b-modal> </b-modal>
<!-- move modal --> <!-- move modal -->
<b-modal class="modal" <b-modal class="modal"
@ -220,39 +144,29 @@ 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 _debounce from 'lodash/debounce'
import {ToastMixin} from "@/utils/utils"; import {ToastMixin} from "@/utils/utils";
import {ApiApiFactory} from "@/utils/openapi/api.ts"; import {ApiApiFactory} from "@/utils/openapi/api.ts";
// import FoodCard from "@/components/FoodCard"; import GenericSplitLists from "@/components/GenericSplitLists";
import GenericHorizontalCard from "@/components/GenericHorizontalCard"; import GenericHorizontalCard from "@/components/GenericHorizontalCard";
import GenericMultiselect from "@/components/GenericMultiselect"; import GenericMultiselect from "@/components/GenericMultiselect";
import InfiniteLoading from 'vue-infinite-loading';
Vue.use(BootstrapVue) Vue.use(BootstrapVue)
export default { export default {
name: 'FoodListView', name: 'FoodListView',
mixins: [ToastMixin], mixins: [ToastMixin, GenericSplitLists, GenericHorizontalCard],
components: {GenericHorizontalCard, GenericMultiselect, InfiniteLoading}, components: {GenericHorizontalCard, GenericMultiselect, GenericSplitLists},
data() { data() {
return { return {
this_model: 'Food',
foods: [], foods: [],
foods2: [], foods2: [],
show_split: false, load_more_left: true,
search_input: '', load_more_right: true,
search_input2: '', blank_item: {
advanced_visible: false, 'id': undefined,
right_page: 0,
right: +new Date(),
isDirtyRight: false,
left_page: 0,
update_recipe: [],
left: +new Date(),
isDirtyLeft: false,
this_item: {
'id': -1,
'name': '', 'name': '',
'description': '', 'description': '',
'recipe': null, 'recipe': null,
@ -260,84 +174,134 @@ export default {
'ignore_shopping': false, 'ignore_shopping': false,
'supermarket_category': undefined, 'supermarket_category': undefined,
'target': { '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': '' '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: { methods: {
resetSearch: function () { // TODO should model actions be included with the context menu? the card? a seperate mixin avaible to all?
if (this.search_input !== '') { resetList: function(e) {
this.search_input = '' if (e.column === 'left') {
} else {
this.left_page = 0
this.foods = [] this.foods = []
this.left += 1 } else if (e.column === 'right') {
}
if (this.search_input2 !== '') {
this.search_input2 = ''
} else {
this.right_page = 0
this.foods2 = [] 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, param) {
startAction: function(e, col) { let source = e?.source ?? this.blank_item
let target = e?.target let target = e?.target ?? undefined
let source = e?.source
if (e.action == 'delete') {
this.this_item = source this.this_item = source
this.this_item.target = target || undefined
switch (e.action) {
case 'delete':
this.$bvModal.show('id_modal_food_delete') this.$bvModal.show('id_modal_food_delete')
} else if (e.action == 'new') { break;
this.this_item = {} case 'new':
this.this_item = {...this.blank_item}
this.$bvModal.show('id_modal_food_edit') this.$bvModal.show('id_modal_food_edit')
} else if (e.action == 'edit') { break;
this.this_item = source case 'edit':
this.$bvModal.show('id_modal_food_edit') this.$bvModal.show('id_modal_food_edit')
} else if (e.action === 'move') { break;
this.this_item = source case 'move':
if (target == null) { if (target == null) {
this.$bvModal.show('id_modal_food_move') this.$bvModal.show('id_modal_food_move')
} else { } else {
this.moveFood(source.id, target.id) this.moveFood(source.id, target.id)
} }
} else if (e.action === 'merge') { break;
this.this_item = source case 'merge':
if (target == null) { if (target == null) {
this.$bvModal.show('id_modal_food_merge') this.$bvModal.show('id_modal_food_merge')
} else { } else {
this.mergeFood(e.source.id, e.target.id) this.mergeFood(e.source.id, e.target.id)
} }
} else if (e.action === 'get-children') { break;
case 'get-children':
if (source.show_children) { if (source.show_children) {
Vue.set(source, 'show_children', false) Vue.set(source, 'show_children', false)
} else { } else {
this.this_item = source this.getChildren(param, source)
this.getChildren(col, source)
} }
} else if (e.action === 'get-recipes') { break;
case 'get-recipes':
if (source.show_recipes) { if (source.show_recipes) {
Vue.set(source, 'show_recipes', false) Vue.set(source, 'show_recipes', false)
} else { } else {
this.this_item = source this.getRecipes(param, source)
this.getRecipes(col, 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 () { saveFood: function () {
let apiClient = new ApiApiFactory() let apiClient = new ApiApiFactory()
@ -358,47 +322,30 @@ export default {
} else { } else {
this.foods2 = [] this.foods2 = []
} }
this.this_item={}
}).catch((err) => { }).catch((err) => {
console.log(err) console.log(err)
this.this_item = {}
}) })
} else { } else {
apiClient.partialUpdateFood(this.this_item.id, food).then(result => { apiClient.partialUpdateFood(this.this_item.id, food).then(result => {
this.refreshCard(this.this_item.id) this.refreshCard(this.this_item.id)
this.this_item={}
}).catch((err) => { }).catch((err) => {
console.log(err) console.log(err)
this.this_item = {}
}) })
} }
}, this.this_item = {...this.blank_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) { moveFood: function (source_id, target_id) {
let apiClient = new ApiApiFactory() let apiClient = new ApiApiFactory()
apiClient.moveFood(String(source_id), String(target_id)).then(result => { apiClient.moveFood(String(source_id), String(target_id)).then(result => {
if (target_id === 0) { 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 food.parent = null
if (this.show_split){ this.foods = [food].concat(this.destroyCard(source_id, this.foods))
this.destroyCard(source_id) // order matters, destroy old card before adding it back in at root this.foods2 = [...food].concat(this.destroyCard(source_id, this.foods2)) // 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 { } else {
this.destroyCard(source_id) this.foods = this.destroyCard(source_id, this.foods)
this.foods = [food].concat(this.foods) this.foods2 = this.destroyCard(source_id, this.foods2)
this.foods2 = []
}
} else {
this.destroyCard(source_id)
this.refreshCard(target_id) this.refreshCard(target_id)
} }
}).catch((err) => { }).catch((err) => {
@ -411,7 +358,7 @@ export default {
mergeFood: function (source_id, target_id) { mergeFood: function (source_id, target_id) {
let apiClient = new ApiApiFactory() let apiClient = new ApiApiFactory()
apiClient.mergeFood(String(source_id), String(target_id)).then(result => { apiClient.mergeFood(String(source_id), String(target_id)).then(result => {
this.destroyCard(source_id) // this.destroyCard(source_id)
this.refreshCard(target_id) this.refreshCard(target_id)
}).catch((err) => { }).catch((err) => {
console.log('Error', err) console.log('Error', err)
@ -430,9 +377,9 @@ export default {
apiClient.listFoods(query, root, tree, page, pageSize).then(result => { apiClient.listFoods(query, root, tree, page, pageSize).then(result => {
if (col == 'left') { if (col == 'left') {
parent = this.findFood(this.foods, food.id) parent = this.findCard(this.foods, food.id)
} else if (col == 'right'){ } else if (col == 'right'){
parent = this.findFood(this.foods2, food.id) parent = this.findCard(this.foods2, food.id)
} }
if (parent) { if (parent) {
Vue.set(parent, 'children', result.data.results) Vue.set(parent, 'children', result.data.results)
@ -454,9 +401,9 @@ export default {
undefined, undefined, undefined, undefined, undefined, pageSize, undefined undefined, undefined, undefined, undefined, undefined, pageSize, undefined
).then(result => { ).then(result => {
if (col == 'left') { if (col == 'left') {
parent = this.findFood(this.foods, food.id) parent = this.findCard(this.foods, food.id)
} else if (col == 'right'){ } else if (col == 'right'){
parent = this.findFood(this.foods2, food.id) parent = this.findCard(this.foods2, food.id)
} }
if (parent) { if (parent) {
Vue.set(parent, 'recipes', result.data.results) Vue.set(parent, 'recipes', result.data.results)
@ -475,11 +422,11 @@ export default {
let idx = undefined let idx = undefined
let idx2 = undefined let idx2 = undefined
apiClient.retrieveFood(id).then(result => { 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) { if (target.parent) {
let parent = this.findFood(this.foods, target.parent) let parent = this.findCard(this.foods, target.parent)
let parent2 = this.findFood(this.foods2, target.parent) let parent2 = this.findCard(this.foods2, target.parent)
if (parent) { if (parent) {
if (parent.show_children){ 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? // this would move with modals with mixin?
prepareEmoji: function() { prepareEmoji: function() {
this.$refs._edit.addText(this.this_item.icon || ''); this.$refs._edit.addText(this.this_item.icon || '');
@ -532,73 +460,15 @@ export default {
setIcon: function(icon) { setIcon: function(icon) {
this.this_item.icon = icon this.this_item.icon = icon
}, },
infiniteHandler: function($state, col) { deleteThis: function(id, model) {
let apiClient = new ApiApiFactory() const result = new Promise((callback) => this.deleteObject(id, model, callback))
let query = (col==='left') ? this.search_input : this.search_input2 result.then(() => {
let page = (col==='left') ? this.left_page + 1 : this.right_page + 1 this.foods = this.destroyCard(id, this.foods)
let root = undefined this.foods2 = this.destroyCard(id, this.foods2)
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)
}, },
} }
} }

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

View File

@ -11,7 +11,7 @@
@drop="handleDragDrop($event)"> @drop="handleDragDrop($event)">
<b-row no-gutters style="height:inherit;"> <b-row no-gutters style="height:inherit;">
<b-col no-gutters md="3" 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>
<b-col no-gutters md="9" style="height:inherit;"> <b-col no-gutters md="9" style="height:inherit;">
<b-card-body class="m-0 py-0" style="height:inherit;"> <b-card-body class="m-0 py-0" style="height:inherit;">
@ -35,7 +35,6 @@
</b-col> </b-col>
<div class="card-img-overlay justify-content-right h-25 m-0 p-0 text-right"> <div class="card-img-overlay justify-content-right h-25 m-0 p-0 text-right">
<slot name="upper-right"></slot> <slot name="upper-right"></slot>
<generic-context-menu class="p-0" <generic-context-menu class="p-0"
:show_merge="merge" :show_merge="merge"
:show_move="move" :show_move="move"
@ -92,6 +91,7 @@
</template> </template>
<script> <script>
import {ApiApiFactory} from "@/utils/openapi/api.ts";
import GenericContextMenu from "@/components/GenericContextMenu"; import GenericContextMenu from "@/components/GenericContextMenu";
import RecipeCard from "@/components/RecipeCard"; import RecipeCard from "@/components/RecipeCard";
import { mixin as clickaway } from 'vue-clickaway'; import { mixin as clickaway } from 'vue-clickaway';
@ -113,6 +113,7 @@ export default {
recipes: {type: String, default: 'recipes'}, recipes: {type: String, default: 'recipes'},
merge: {type: Boolean, default: false}, merge: {type: Boolean, default: false},
move: {type: Boolean, default: false}, move: {type: Boolean, default: false},
tree: {type: Boolean, default: false},
}, },
data() { data() {
return { return {
@ -124,7 +125,6 @@ export default {
source: {'id': undefined, 'name': undefined}, source: {'id': undefined, 'name': undefined},
target: {'id': undefined, 'name': undefined}, target: {'id': undefined, 'name': undefined},
text: { text: {
'image_alt': '',
'hide_children': '', 'hide_children': '',
} }
} }
@ -132,8 +132,7 @@ export default {
mounted() { mounted() {
this.model_image = this.model?.image ?? window.IMAGE_PLACEHOLDER this.model_image = this.model?.image ?? window.IMAGE_PLACEHOLDER
this.dragMenu = this.$refs.tooltip this.dragMenu = this.$refs.tooltip
this.text.image_alt = this.$t(this.model_name + '_Image'), this.text.hide_children = this.$t('Hide_' + this.model_name)
this.hide_children = this.$t('Hide_' + this.model_name)
}, },
methods: { methods: {
emitAction: function(m) { emitAction: function(m) {
@ -199,7 +198,39 @@ export default {
}, },
closeMenu: function(){ closeMenu: function(){
this.show_menu = false 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> </script>

View File

@ -66,7 +66,7 @@ export default {
let page = 1 let page = 1
let root = undefined let root = undefined
let tree = undefined let tree = undefined
let pageSize = 25 let pageSize = 10
if (query === '') { if (query === '') {
query = undefined query = undefined

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

View File

@ -108,5 +108,7 @@
"Edit_Food": "Edit Food", "Edit_Food": "Edit Food",
"Move_Food": "Move Food", "Move_Food": "Move Food",
"New_Food": "New Food", "New_Food": "New Food",
"Hide_Foods": "Hide Food" "Hide_Food": "Hide Food",
"Delete_Food": "Delete Food",
"No_ID": "ID not found, cannot delete."
} }

File diff suppressed because it is too large Load Diff