finished food view

This commit is contained in:
smilerz 2021-08-31 12:57:25 -05:00
parent 52abba1f16
commit 65fbbabec5
22 changed files with 430 additions and 155454 deletions

View File

@ -1,3 +1,4 @@
from cookbook.models import SearchFields
from django.db import migrations from django.db import migrations

View File

@ -1 +1 @@
.shake[data-v-8f249282]{-webkit-animation:shake-data-v-8f249282 .82s cubic-bezier(.36,.07,.19,.97) both;animation:shake-data-v-8f249282 .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-8f249282{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-8f249282{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-6aec55e4]{-webkit-animation:shake-data-v-6aec55e4 .82s cubic-bezier(.36,.07,.19,.97) both;animation:shake-data-v-6aec55e4 .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-6aec55e4{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-6aec55e4{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-8f249282]{-webkit-animation:shake-data-v-8f249282 .82s cubic-bezier(.36,.07,.19,.97) both;animation:shake-data-v-8f249282 .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-8f249282{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-8f249282{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-6aec55e4]{-webkit-animation:shake-data-v-6aec55e4 .82s cubic-bezier(.36,.07,.19,.97) both;animation:shake-data-v-6aec55e4 .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-6aec55e4{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-6aec55e4{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

File diff suppressed because one or more lines are too long

View File

@ -242,6 +242,9 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin):
except (PathOverflow, InvalidMoveToDescendant, InvalidPosition): except (PathOverflow, InvalidMoveToDescendant, InvalidPosition):
content = {'error': True, 'msg': _('An error occurred attempting to move ') + child.name} content = {'error': True, 'msg': _('An error occurred attempting to move ') + child.name}
return Response(content, status=status.HTTP_400_BAD_REQUEST) return Response(content, status=status.HTTP_400_BAD_REQUEST)
elif parent == child.id:
content = {'error': True, 'msg': _('Cannot move an object to itself!')}
return Response(content, status=status.HTTP_403_FORBIDDEN)
try: try:
parent = self.model.objects.get(pk=parent, space=self.request.space) parent = self.model.objects.get(pk=parent, space=self.request.space)

View File

@ -3,131 +3,52 @@
<generic-modal-form <generic-modal-form
:model="this_model" :model="this_model"
:action="this_action" :action="this_action"
:item1="foods[5]" :item1="this_item"
:item2="undefined" :item2="this_target"
:show="true"/> <!-- TODO make this based on method --> :show="show_modal"
@finish-action="finishAction"/>
<generic-split-lists <generic-split-lists
:list_name="this_model.name" :list_name="this_model.name"
@reset="resetList" @reset="resetList"
@get-list="getFoods" @get-list="getItems"
@item-action="startAction" @item-action="startAction"
> >
<template v-slot:cards-left> <template v-slot:cards-left>
<generic-horizontal-card <generic-horizontal-card
v-for="f in foods" v-bind:key="f.id" v-for="i in items_left" v-bind:key="i.id"
:model=f :item=i
:model_name="this_model.name" :item_type="this_model.name"
:draggable="true" :draggable="true"
:merge="true" :merge="true"
:move="true" :move="true"
@item-action="startAction($event, 'left')" @item-action="startAction($event, 'left')"
> >
<!-- foods can also be a recipe, show link to the recipe if it exists -->
<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="i.recipe" v-b-tooltip.hover :title="i.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="i.recipe.url"/>
</template> </template>
</generic-horizontal-card> </generic-horizontal-card>
</template> </template>
<template v-slot:cards-right> <template v-slot:cards-right>
<generic-horizontal-card v-for="f in foods2" v-bind:key="f.id" <generic-horizontal-card v-for="i in items_right" v-bind:key="i.id"
:model=f :item=i
:model_name="this_model.name" :item_type="this_model.name"
:draggable="true" :draggable="true"
:merge="true" :merge="true"
:move="true" :move="true"
@item-action="startAction($event, 'right')" @item-action="startAction($event, 'right')"
> >
<!-- foods can also be a recipe, show link to the recipe if it exists -->
<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="i.recipe" v-b-tooltip.hover :title="i.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="i.recipe.url"/>
</template> </template>
</generic-horizontal-card> </generic-horizontal-card>
</template> </template>
</generic-split-lists> </generic-split-lists>
<!-- 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"
:initial_selection="[this_item.recipe]"
:model="recipe"
: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"
:model="models.SHOPPING_CATEGORY"
: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="deleteThis(this_item.id)">
{{this.$t("delete_confimation", {'kw': 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"
:model="this_model"
:multiple="false"
:sticky_options="[{'id': 0,'name': $t('Root')}]"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
>
</generic-multiselect>
</b-modal>
<!-- merge modal -->
<b-modal class="modal"
:id="'id_modal_food_merge'"
:title="this.$t('merge_title', {'type': '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"
:model="this_model"
:multiple="false"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
:placeholder="this.$t('Search')">
</generic-multiselect>
</b-modal>
</div> </div>
</template> </template>
@ -145,90 +66,72 @@ import {StandardToasts} from "@/utils/utils";
import GenericSplitLists from "@/components/GenericSplitLists"; import GenericSplitLists from "@/components/GenericSplitLists";
import GenericHorizontalCard from "@/components/GenericHorizontalCard"; import GenericHorizontalCard from "@/components/GenericHorizontalCard";
import GenericMultiselect from "@/components/GenericMultiselect";
import GenericModalForm from "@/components/Modals/GenericModalForm"; import GenericModalForm from "@/components/Modals/GenericModalForm";
Vue.use(BootstrapVue) Vue.use(BootstrapVue)
export default { export default {
name: 'FoodListView', name: 'FoodListView', // TODO: make generic name
mixins: [CardMixin, ToastMixin], mixins: [CardMixin, ToastMixin],
components: {GenericHorizontalCard, GenericMultiselect, GenericSplitLists, GenericModalForm}, components: {GenericHorizontalCard, GenericSplitLists, GenericModalForm},
data() { data() {
return { return {
this_model: Models.FOOD, //TODO: mounted method to calcuate items_left: [],
this_action: Actions.UPDATE, //TODO: based on what we are doing items_right: [],
models: Models,
foods: [],
foods2: [],
load_more_left: true, load_more_left: true,
load_more_right: true, load_more_right: true,
blank_item: { this_model: Models.FOOD, //TODO: mounted method to calcuate
'id': undefined, this_action: undefined,
'name': '', this_item: {},
'description': '', this_target: {},
'recipe': null, models: Models,
'recipe_full': undefined, show_modal:false
'ignore_shopping': false,
'supermarket_category': undefined,
'target': {
'id': undefined,
'name': ''
},
},
this_item: {
'id': undefined,
'name': '',
'description': '',
'recipe': null,
'recipe_full': undefined,
'ignore_shopping': false,
'supermarket_category': undefined,
'target': {
'id': undefined,
'name': ''
},
},
} }
}, },
methods: { methods: {
// TODO should model actions be included with the context menu? the card? a seperate mixin avaible to all?
resetList: function(e) { resetList: function(e) {
if (e.column === 'left') { if (e.column === 'left') {
this.foods = [] this.items_left = []
} else if (e.column === 'right') { } else if (e.column === 'right') {
this.foods2 = [] this.items_right = []
} }
}, },
startAction: function(e, param) { startAction: function(e, param) {
let source = e?.source ?? this.blank_item let source = e?.source ?? {}
let target = e?.target ?? undefined let target = e?.target ?? undefined
this.this_item = source this.this_item = source
this.this_item.target = target || undefined this.this_target = target
switch (e.action) { switch (e.action) {
case 'delete': case 'delete':
this.$bvModal.show('id_modal_food_delete') this.this_action = Actions.DELETE
this.show_modal = true
break; break;
case 'new': case 'new':
this.this_item = {...this.blank_item} this.this_action = Actions.CREATE
this.$bvModal.show('id_modal_food_edit') this.show_modal = true
break; break;
case 'edit': case 'edit':
this.$bvModal.show('id_modal_food_edit') this.this_item = e.source
this.this_action = Actions.UPDATE
this.show_modal = true
break; break;
case 'move': case 'move':
if (target == null) { if (target == null) {
this.$bvModal.show('id_modal_food_move') this.this_item = e.source
this.this_action = Actions.MOVE
this.show_modal = true
} else { } else {
this.moveFood(source.id, target.id) this.moveThis(source.id, target.id)
} }
break; break;
case 'merge': case 'merge':
if (target == null) { if (target == null) {
this.$bvModal.show('id_modal_food_merge') this.this_item = e.source
this.this_action = Actions.MERGE
this.show_modal = true
} else { } else {
this.mergeFood(e.source.id, e.target.id) this.mergeThis(e.source.id, e.target.id)
} }
break; break;
case 'get-children': case 'get-children':
@ -247,25 +150,51 @@ export default {
break; break;
} }
}, },
getFoods: function(params, callback) { finishAction: function(e) {
let update = undefined
if (e !== 'cancel') {
switch(this.this_action) {
case Actions.DELETE:
this.deleteThis(this.this_item.id)
break;
case Actions.CREATE:
this.saveThis(e.form_data)
break;
case Actions.UPDATE:
update = e.form_data
update.id = this.this_item.id
this.saveThis(update)
break;
case Actions.MERGE:
this.mergeThis(this.this_item.id, e.form_data.target)
break;
case Actions.MOVE:
this.moveThis(this.this_item.id, e.form_data.target)
break;
}
}
this.clearState()
},
getItems: function(params, callback) {
let column = params?.column ?? 'left' let column = params?.column ?? 'left'
// TODO: does this need to be a callback? // TODO: does this need to be a callback?
genericAPI(this.this_model, Actions.LIST, params).then((result) => { genericAPI(this.this_model, Actions.LIST, params).then((result) => {
if (result.data.results.length){ if (result.data.results.length){
if (column ==='left') { if (column ==='left') {
this.foods = this.foods.concat(result.data.results) // if paginated results are in result.data.results otherwise just result.data
this.items_left = this.items_left.concat(result.data?.results ?? result.data)
} else if (column ==='right') { } else if (column ==='right') {
this.foods2 = this.foods2.concat(result.data.results) this.items_right = this.items_right.concat(result.data?.results ?? result.data)
} }
// are the total elements less than the length of the array? if so, stop loading // are the total elements less than the length of the array? if so, stop loading
callback(result.data.count > (column==="left" ? this.foods.length : this.foods2.length)) callback(result.data.count > (column==="left" ? this.items_left.length : this.items_right.length))
} else { } else {
callback(false) // stop loading callback(false) // stop loading
console.log('no data returned') console.log('no data returned')
} }
// return true if total objects are still less than the length of the list // return true if total objects are still less than the length of the list
callback(result.data.count < (column==="left" ? this.foods.length : this.foods2.length)) // TODO this needs generalized to handle non-paginated data
callback(result.data.count < (column==="left" ? this.items_left.length : this.items_right.length))
}).catch((err) => { }).catch((err) => {
console.log(err) console.log(err)
@ -275,50 +204,52 @@ export default {
getThis: function(id, callback){ getThis: function(id, callback){
return genericAPI(this.this_model, Actions.FETCH, {'id': id}) return genericAPI(this.this_model, Actions.FETCH, {'id': id})
}, },
saveFood: function () { saveThis: function (thisItem) {
let food = {...this.this_item} if (!thisItem?.id) { // if there is no item id assume it's a new item
food.supermarket_category = this.this_item.supermarket_category?.id ?? null genericAPI(this.this_model, Actions.CREATE, thisItem).then((result) => {
food.recipe = this.this_item.recipe?.id ?? null // place all new items at the top of the list - could sort instead
if (!food?.id) { // if there is no item id assume it's a new item this.items_left = [result.data].concat(this.items_left)
genericAPI(this.this_model, Actions.CREATE, 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 // this creates a deep copy to make sure that columns stay independent
if (this.show_split){ this.items_right = [{...result.data}].concat(this.items_right)
this.foods2 = [...this.foods]
} else {
this.foods2 = []
}
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE) StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
}).catch((err) => { }).catch((err) => {
console.log(err) console.log(err)
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE) StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
}) })
} else { } else {
genericAPI(this.this_model, Actions.UPDATE, food).then((result) => { genericAPI(this.this_model, Actions.UPDATE, thisItem).then((result) => {
this.refreshObject(food.id) this.refreshThis(thisItem.id)
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE) StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE)
}).catch((err) => { }).catch((err) => {
console.log(err) console.log(err)
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE) StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
}) })
} }
this.this_item = {...this.blank_item}
}, },
moveFood: function (source_id, target_id) { moveThis: function (source_id, target_id) {
if (source_id === target_id) {
this.makeToast(this.$t('Error'), this.$t('Cannot move item to itself'), 'danger')
this.clearState()
return
}
if (!source_id || !target_id) {
this.makeToast(this.$t('Warning'), this.$t('Nothing to do'), 'warning')
this.clearState()
return
}
genericAPI(this.this_model, Actions.MOVE, {'source': source_id, 'target': target_id}).then((result) => { genericAPI(this.this_model, Actions.MOVE, {'source': source_id, 'target': target_id}).then((result) => {
if (target_id === 0) { if (target_id === 0) {
let food = this.findCard(source_id, this.foods) || this.findCard(source_id, this.foods2) let item = this.findCard(source_id, this.items_left) || this.findCard(source_id, this.items_right)
this.foods = [food].concat(this.destroyCard(source_id, this.foods)) // order matters, destroy old card before adding it back in at root this.items_left = [item].concat(this.destroyCard(source_id, this.items_left)) // order matters, destroy old card before adding it back in at root
this.foods2 = [...[food]].concat(this.destroyCard(source_id, this.foods2)) // order matters, destroy old card before adding it back in at root this.items_right = [...[item]].concat(this.destroyCard(source_id, this.items_right)) // order matters, destroy old card before adding it back in at root
food.parent = null item.parent = null
} else { } else {
this.foods = this.destroyCard(source_id, this.foods) this.items_left = this.destroyCard(source_id, this.items_left)
this.foods2 = this.destroyCard(source_id, this.foods2) this.items_right = this.destroyCard(source_id, this.items_right)
this.refreshObject(target_id) this.refreshThis(target_id)
} }
// TODO make standard toast // TODO make standard toast
this.makeToast(this.$t('Success'), 'Succesfully moved food', 'success') this.makeToast(this.$t('Success'), 'Succesfully moved resource', 'success')
}).catch((err) => { }).catch((err) => {
// TODO none of the error checking works because the openapi generated functions don't throw an error? // TODO none of the error checking works because the openapi generated functions don't throw an error?
// or i'm capturing it incorrectly // or i'm capturing it incorrectly
@ -326,26 +257,38 @@ export default {
this.makeToast(this.$t('Error'), err.bodyText, 'danger') this.makeToast(this.$t('Error'), err.bodyText, 'danger')
}) })
}, },
mergeFood: function (source_id, target_id) { mergeThis: function (source_id, target_id) {
if (source_id === target_id) {
this.makeToast(this.$t('Error'), this.$t('Cannot merge item with itself'), 'danger')
this.clearState()
return
}
if (!source_id || !target_id) {
this.makeToast(this.$t('Warning'), this.$t('Nothing to do'), 'warning')
this.clearState()
return
}
genericAPI(this.this_model, Actions.MERGE, {'source': source_id, 'target': target_id}).then((result) => { genericAPI(this.this_model, Actions.MERGE, {'source': source_id, 'target': target_id}).then((result) => {
this.foods = this.destroyCard(source_id, this.foods) this.items_left = this.destroyCard(source_id, this.items_left)
this.foods2 = this.destroyCard(source_id, this.foods2) this.items_right = this.destroyCard(source_id, this.items_right)
this.refreshObject(target_id) this.refreshThis(target_id)
// TODO make standard toast
this.makeToast(this.$t('Success'), 'Succesfully merged resource', 'success')
}).catch((err) => { }).catch((err) => {
//TODO error checking not working with OpenAPI methods
console.log('Error', err) console.log('Error', err)
this.makeToast(this.$t('Error'), err.bodyText, 'danger') this.makeToast(this.$t('Error'), err.bodyText, 'danger')
}) })
// TODO make standard toast
this.makeToast(this.$t('Success'), 'Succesfully merged food', 'success')
}, },
getChildren: function(col, food){ getChildren: function(col, item){
let parent = {} let parent = {}
let options = { let options = {
'root': food.id, 'root': item.id,
'pageSize': 200 'pageSize': 200
} }
genericAPI(this.this_model, Actions.LIST, options).then((result) => { genericAPI(this.this_model, Actions.LIST, options).then((result) => {
parent = this.findCard(food.id, col === 'left' ? this.foods : this.foods2) parent = this.findCard(item.id, col === 'left' ? this.items_left : this.items_right)
if (parent) { if (parent) {
Vue.set(parent, 'children', result.data.results) Vue.set(parent, 'children', result.data.results)
Vue.set(parent, 'show_children', true) Vue.set(parent, 'show_children', true)
@ -358,13 +301,13 @@ export default {
}, },
getRecipes: function(col, food){ getRecipes: function(col, food){
let parent = {} let parent = {}
// TODO: make this generic
let options = { let options = {
'foods': food.id, 'foods': food.id,
'pageSize': 200 'pageSize': 200
} }
genericAPI(Models.RECIPE, Actions.LIST, options).then((result) => { genericAPI(Models.RECIPE, Actions.LIST, options).then((result) => {
parent = this.findCard(food.id, col === 'left' ? this.foods : this.foods2) parent = this.findCard(food.id, col === 'left' ? this.items_left : this.items_right)
if (parent) { if (parent) {
Vue.set(parent, 'recipes', result.data.results) Vue.set(parent, 'recipes', result.data.results)
Vue.set(parent, 'show_recipes', true) Vue.set(parent, 'show_recipes', true)
@ -376,32 +319,28 @@ export default {
this.makeToast(this.$t('Error'), err.bodyText, 'danger') this.makeToast(this.$t('Error'), err.bodyText, 'danger')
}) })
}, },
refreshObject: function(id){ refreshThis: function(id){
this.getThis(id).then(result => { this.getThis(id).then(result => {
this.refreshCard(result.data, this.foods) this.refreshCard(result.data, this.items_left)
this.refreshCard({...result.data}, this.foods2) this.refreshCard({...result.data}, this.items_right)
}) })
}, },
// this would move with modals with mixin? deleteThis: function(id) {
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
},
deleteThis: function(id, model) {
genericAPI(this.this_model, Actions.DELETE, {'id': id}).then((result) => { genericAPI(this.this_model, Actions.DELETE, {'id': id}).then((result) => {
this.foods = this.destroyCard(id, this.foods) this.items_left = this.destroyCard(id, this.items_left)
this.foods2 = this.destroyCard(id, this.foods2) this.items_right = this.destroyCard(id, this.items_right)
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_DELETE) StandardToasts.makeStandardToast(StandardToasts.SUCCESS_DELETE)
}).catch((err) => { }).catch((err) => {
console.log(err) console.log(err)
StandardToasts.makeStandardToast(StandardToasts.FAIL_DELETE) StandardToasts.makeStandardToast(StandardToasts.FAIL_DELETE)
}) })
}, },
clearState: function() {
this.show_modal = false
this.this_action = undefined
this.this_item = undefined
this.this_target = undefined
}
} }
} }

View File

@ -11,22 +11,22 @@
@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="$t('Recipe_Image')"></b-card-img-lazy> <b-card-img-lazy style="object-fit: cover; height: 10vh;" :src="item_image" v-bind:alt="$t('Recipe_Image')"></b-card-img-lazy>
</b-col> </b-col>
<b-col no-gutters md="9" style="height:inherit;"> <b-col no-gutters 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;">
<b-card-text class=" h-100 my-0 d-flex flex-column" style="text-overflow: ellipsis"> <b-card-text class=" h-100 my-0 d-flex flex-column" style="text-overflow: ellipsis">
<h5 class="m-0 mt-1 text-truncate">{{ model[title] }}</h5> <h5 class="m-0 mt-1 text-truncate">{{ item[title] }}</h5>
<div class= "m-0 text-truncate">{{ model[subtitle] }}</div> <div class= "m-0 text-truncate">{{ item[subtitle] }}</div>
<div class="mt-auto mb-1 d-flex flex-row justify-content-end"> <div class="mt-auto mb-1 d-flex flex-row justify-content-end">
<div v-if="model[child_count] !=0" class="mx-2 btn btn-link btn-sm" <div v-if="item[child_count] !=0" class="mx-2 btn btn-link btn-sm"
style="z-index: 800;" v-on:click="$emit('item-action',{'action':'get-children','source':model})"> style="z-index: 800;" v-on:click="$emit('item-action',{'action':'get-children','source':item})">
<div v-if="!model.show_children">{{ model[child_count] }} {{ model_name }}</div> <div v-if="!item.show_children">{{ item[child_count] }} {{ item_type }}</div>
<div v-else>{{ text.hide_children }}</div> <div v-else>{{ text.hide_children }}</div>
</div> </div>
<div v-if="model[recipe_count]" class="mx-2 btn btn-link btn-sm" style="z-index: 800;" <div v-if="item[recipe_count]" class="mx-2 btn btn-link btn-sm" style="z-index: 800;"
v-on:click="$emit('item-action',{'action':'get-recipes','source':model})"> v-on:click="$emit('item-action',{'action':'get-recipes','source':item})">
<div v-if="!model.show_recipes">{{ model[recipe_count] }} {{$t('Recipes')}}</div> <div v-if="!item.show_recipes">{{ item[recipe_count] }} {{$t('Recipes')}}</div>
<div v-else>{{$t('Hide_Recipes')}}</div> <div v-else>{{$t('Hide_Recipes')}}</div>
</div> </div>
</div> </div>
@ -38,18 +38,18 @@
<generic-context-menu class="p-0" <generic-context-menu class="p-0"
:show_merge="merge" :show_merge="merge"
:show_move="move" :show_move="move"
@item-action="$emit('item-action', {'action': $event, 'source': model})"> @item-action="$emit('item-action', {'action': $event, 'source': item})">
</generic-context-menu> </generic-context-menu>
</div> </div>
</b-row> </b-row>
</b-card> </b-card>
<!-- recursively add child cards --> <!-- recursively add child cards -->
<div class="row" v-if="model.show_children"> <div class="row" v-if="item.show_children">
<div class="col-md-11 offset-md-1"> <div class="col-md-11 offset-md-1">
<generic-horizontal-card v-for="child in model[children]" v-bind:key="child.id" <generic-horizontal-card v-for="child in item[children]" v-bind:key="child.id"
:draggable="draggable" :draggable="draggable"
:model="child" :item="child"
:model_name="model_name" :item_type="item_type"
:title="title" :title="title"
:subtitle="subtitle" :subtitle="subtitle"
:child_count="child_count" :child_count="child_count"
@ -63,10 +63,10 @@
</div> </div>
</div> </div>
<!-- conditionally view recipes --> <!-- conditionally view recipes -->
<div class="row" v-if="model.show_recipes"> <div class="row" v-if="item.show_recipes">
<div class="col-md-11 offset-md-1"> <div class="col-md-11 offset-md-1">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));grid-gap: 1rem;"> <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));grid-gap: 1rem;">
<recipe-card v-for="r in model[recipes]" <recipe-card v-for="r in item[recipes]"
v-bind:key="r.id" v-bind:key="r.id"
:recipe="r"> :recipe="r">
</recipe-card> </recipe-card>
@ -75,11 +75,11 @@
</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:9999; cursor:pointer"> <b-list-group ref="tooltip" variant="light" v-show="show_menu" v-on-clickaway="closeMenu" style="z-index:9999; cursor:pointer">
<b-list-group-item v-if="move" action v-on:click="$emit('item-action',{'action': 'move', 'target': model, 'source': source}); closeMenu()"> <b-list-group-item v-if="move" action v-on:click="$emit('item-action',{'action': 'move', 'target': item, 'source': source}); closeMenu()">
{{$t('Move')}}: {{$t('move_confirmation', {'child': source.name,'parent':model.name})}} <i class="fas fa-expand-arrows-alt fa-fw"></i> {{$t('Move')}}: {{$t('move_confirmation', {'child': source.name,'parent':item.name})}}
</b-list-group-item> </b-list-group-item>
<b-list-group-item v-if="merge" action v-on:click="$emit('item-action',{'action': 'merge', 'target': model, 'source': source}); closeMenu()"> <b-list-group-item v-if="merge" action v-on:click="$emit('item-action',{'action': 'merge', 'target': item, 'source': source}); closeMenu()">
{{$t('Merge')}}: {{ $t('merge_confirmation', {'source': source.name,'target':model.name}) }} <i class="fas fa-compress-arrows-alt fa-fw"></i> {{$t('Merge')}}: {{ $t('merge_confirmation', {'source': source.name,'target':item.name}) }}
</b-list-group-item> </b-list-group-item>
<b-list-group-item action v-on:click="closeMenu()"> <b-list-group-item action v-on:click="closeMenu()">
{{$t('Cancel')}} {{$t('Cancel')}}
@ -101,8 +101,8 @@ export default {
components: { GenericContextMenu, RecipeCard }, components: { GenericContextMenu, RecipeCard },
mixins: [clickaway], mixins: [clickaway],
props: { props: {
model: Object, item: Object,
model_name: {type: String, default: 'Blank Model'}, // TODO update translations to handle plural translations item_type: {type: String, default: 'Blank Item Type'}, // TODO update translations to handle plural translations
draggable: {type: Boolean, default: false}, draggable: {type: Boolean, default: false},
title: {type: String, default: 'name'}, title: {type: String, default: 'name'},
subtitle: {type: String, default: 'description'}, subtitle: {type: String, default: 'description'},
@ -115,7 +115,7 @@ export default {
}, },
data() { data() {
return { return {
model_image: '', item_image: '',
over: false, over: false,
show_menu: false, show_menu: false,
dragMenu: undefined, dragMenu: undefined,
@ -128,14 +128,14 @@ export default {
} }
}, },
mounted() { mounted() {
this.model_image = this.model?.image ?? window.IMAGE_PLACEHOLDER this.item_image = this.item?.image ?? window.IMAGE_PLACEHOLDER
this.dragMenu = this.$refs.tooltip this.dragMenu = this.$refs.tooltip
this.text.hide_children = this.$t('Hide_' + this.model_name) this.text.hide_children = this.$t('Hide_' + this.item_type)
}, },
methods: { methods: {
handleDragStart: function(e) { handleDragStart: function(e) {
this.isError = false this.isError = false
e.dataTransfer.setData('source', JSON.stringify(this.model)) e.dataTransfer.setData('source', JSON.stringify(this.item))
}, },
handleDragEnter: function(e) { handleDragEnter: function(e) {
if (!e.currentTarget.contains(e.relatedTarget) && e.relatedTarget != null) { if (!e.currentTarget.contains(e.relatedTarget) && e.relatedTarget != null) {
@ -149,7 +149,7 @@ export default {
}, },
handleDragDrop: function(e) { handleDragDrop: function(e) {
let source = JSON.parse(e.dataTransfer.getData('source')) let source = JSON.parse(e.dataTransfer.getData('source'))
if (source.id != this.model.id){ if (source.id != this.item.id){
this.source = source this.source = source
let menuLocation = {getBoundingClientRect: this.generateLocation(e.clientX, e.clientY),} let menuLocation = {getBoundingClientRect: this.generateLocation(e.clientX, e.clientY),}
this.show_menu = true this.show_menu = true
@ -176,7 +176,7 @@ export default {
}) })
popper.update() popper.update()
this.over = false this.over = false
this.$emit({'action': 'drop', 'target': this.model, 'source': this.source}) this.$emit({'action': 'drop', 'target': this.item, 'source': this.source})
} else { } else {
this.isError = true this.isError = true
} }

View File

@ -76,7 +76,7 @@
<template v-slot:no-more><span/></template> <template v-slot:no-more><span/></template>
</infinite-loading> </infinite-loading>
</div> </div>
<!-- right side food cards --> <!-- right side cards -->
<div class="col col-md mh-100 overflow-auto" v-if="show_split"> <div class="col col-md mh-100 overflow-auto" v-if="show_split">
<slot name="cards-right"></slot> <slot name="cards-right"></slot>
<infinite-loading <infinite-loading
@ -119,8 +119,6 @@ export default {
left_page: 0, left_page: 0,
right: +new Date(), right: +new Date(),
left: +new Date(), left: +new Date(),
isDirtyright: false,
isDirtyleft: false,
text: { text: {
'new': '', 'new': '',
'name': '', 'name': '',
@ -133,7 +131,6 @@ export default {
mounted() { mounted() {
this.dragMenu = this.$refs.tooltip this.dragMenu = this.$refs.tooltip
this.text.new = this.$t('New_' + this.list_name) this.text.new = this.$t('New_' + this.list_name)
this.text.name = this.$t(this.list_name)
}, },
watch: { watch: {
search_right: _debounce(function() { search_right: _debounce(function() {
@ -170,6 +167,7 @@ export default {
'page': (col==='left') ? this.left_page + 1 : this.right_page + 1, 'page': (col==='left') ? this.left_page + 1 : this.right_page + 1,
'column': col 'column': col
} }
// TODO: change this to be an emit and watch a prop to determine if loaded or complete
new Promise((callback) => this.$emit('get-list', params, callback)).then((result) => { new Promise((callback) => this.$emit('get-list', params, callback)).then((result) => {
this[col+'_page']+=1 this[col+'_page']+=1
$state.loaded(); $state.loaded();

View File

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<b-modal class="modal" id="modal" > <b-modal class="modal" id="modal" @hidden="cancelAction">
<template v-slot:modal-title><h4>{{form.title}}</h4></template> <template v-slot:modal-title><h4>{{form.title}}</h4></template>
<div v-for="(f, i) in form.fields" v-bind:key=i> <div v-for="(f, i) in form.fields" v-bind:key=i>
<p v-if="f.type=='instruction'">{{f.label}}</p> <p v-if="f.type=='instruction'">{{f.label}}</p>
@ -10,7 +10,8 @@
:field="f.field" :field="f.field"
:model="listModel(f.list)" :model="listModel(f.list)"
:sticky_options="f.sticky_options || undefined" :sticky_options="f.sticky_options || undefined"
@change="changeValue"/> <!-- TODO add ability to create new items associated with lookup --> @change="storeValue"/> <!-- TODO add ability to create new items associated with lookup -->
<!-- TODO: add emoji field -->
<checkbox-input v-if="f.type=='checkbox'" <checkbox-input v-if="f.type=='checkbox'"
:label="f.label" :label="f.label"
:value="f.value" :value="f.value"
@ -23,11 +24,10 @@
</div> </div>
<template v-slot:modal-footer> <template v-slot:modal-footer>
<b-button class="float-right mx-1" variant="secondary" v-on:click="$bvModal.hide('modal')">{{$t('Cancel')}}</b-button> <b-button class="float-right mx-1" variant="secondary" v-on:click="cancelAction">{{$t('Cancel')}}</b-button>
<b-button class="float-right mx-1" variant="primary" v-on:click="doAction">{{form.ok_label}}</b-button> <b-button class="float-right mx-1" variant="primary" v-on:click="doAction">{{form.ok_label}}</b-button>
</template> </template>
</b-modal> </b-modal>
<b-button v-on:click="Button">ok</b-button>
</div> </div>
</template> </template>
@ -55,19 +55,13 @@ export default {
}, },
data() { data() {
return { return {
new_item: {}, form_data: {},
form: {}, form: {},
buttons: { dirty: false
'new':{'label': this.$t('Save')},
'delete':{'label': this.$t('Delete')},
'edit':{'label': this.$t('Save')},
'move':{'label': this.$t('Move')},
'merge':{'label': this.$t('Merge')}
}
} }
}, },
mounted() { mounted() {
this.$root.$on('change', this.changeValue); // modal is outside Vue instance(?) so have to listen at root of component this.$root.$on('change', this.storeValue); // modal is outside Vue instance(?) so have to listen at root of component
}, },
computed: { computed: {
buttonLabel() { buttonLabel() {
@ -78,30 +72,27 @@ export default {
'show': function () { 'show': function () {
if (this.show) { if (this.show) {
this.form = getForm(this.model, this.action, this.item1, this.item2) this.form = getForm(this.model, this.action, this.item1, this.item2)
this.dirty = true
this.$bvModal.show('modal') this.$bvModal.show('modal')
} else {
this.$bvModal.hide('modal')
this.form_data = {}
} }
}, },
}, },
methods: { methods: {
Button: function(e) {
this.form = getForm(this.model, this.action, this.item1, this.item2)
console.log(this.form)
this.$bvModal.show('modal')
},
doAction: function(){ doAction: function(){
let alert_text = '' this.dirty = false
for (const [k, v] of Object.entries(this.form.fields)) { this.$emit('finish-action', {'form_data': this.form_data })
if (v.type !== 'instruction'){
alert_text = alert_text + v.field + ": " + this.new_item[v.field] + "\n"
}
}
this.$nextTick(function() {this.$bvModal.hide('modal')})
setTimeout(() => {}, 0) // confirm that the
alert(alert_text)
}, },
changeValue: function(field, value) { cancelAction: function() {
// console.log('catch change', field, value) if (this.dirty) {
this.new_item[field] = value this.dirty = false
this.$emit('finish-action', 'cancel')
}
},
storeValue: function(field, value) {
this.form_data[field] = value
}, },
listModel: function(m) { listModel: function(m) {
if (m === 'self') { if (m === 'self') {

View File

@ -120,5 +120,6 @@
"edit_title": "Edit {type}", "edit_title": "Edit {type}",
"Name": "Name", "Name": "Name",
"Description": "Description", "Description": "Description",
"Recipe": "Recipe" "Recipe": "Recipe",
"tree_root": "Root of Tree"
} }

View File

@ -43,6 +43,17 @@ export class Models {
] ]
} }
} }
},
'move': {
'form': {
'target': {
'form_field': true,
'type': 'lookup',
'field': 'target',
'list': 'self',
'sticky_options': [{'id': 0,'name': i18n.t('tree_root')}]
}
}
} }
} }

View File

@ -48,7 +48,7 @@ export class StandardToasts {
makeToast(i18n.tc('Success'), i18n.tc('success_deleting_resource'), 'success') makeToast(i18n.tc('Success'), i18n.tc('success_deleting_resource'), 'success')
break; break;
case StandardToasts.FAIL_CREATE: case StandardToasts.FAIL_CREATE:
makeToast(i18n.tc('Failure'), i18n.tc('success_creating_resource'), 'danger') makeToast(i18n.tc('Failure'), i18n.tc('err_creating_resource'), 'danger')
break; break;
case StandardToasts.FAIL_FETCH: case StandardToasts.FAIL_FETCH:
makeToast(i18n.tc('Failure'), i18n.tc('err_fetching_resource'), 'danger') makeToast(i18n.tc('Failure'), i18n.tc('err_fetching_resource'), 'danger')
@ -167,7 +167,6 @@ export function genericAPI(model, action, options) {
let config = setup?.config ?? {} let config = setup?.config ?? {}
let params = setup?.params ?? [] let params = setup?.params ?? []
let parameters = [] let parameters = []
let this_value = undefined let this_value = undefined
params.forEach(function (item, index) { params.forEach(function (item, index) {
if (Array.isArray(item)) { if (Array.isArray(item)) {
@ -181,8 +180,7 @@ export function genericAPI(model, action, options) {
} }
} }
} else { } else {
this_value = options?.[item] ?? undefined this_value = formatParam(config?.[item], options?.[item] ?? undefined)
if (this_value) {this_value = formatParam(config?.[item], this_value)}
} }
// if no value is found so far, get the default if it exists // if no value is found so far, get the default if it exists
if (!this_value) { if (!this_value) {
@ -272,7 +270,6 @@ export function getForm(model, action, item1, item2) {
let config = {...action?.form, ...model.model_type?.[f]?.form, ...model?.[f]?.form} let config = {...action?.form, ...model.model_type?.[f]?.form, ...model?.[f]?.form}
// if not defined partialUpdate will use form from create // if not defined partialUpdate will use form from create
if (f === 'partialUpdate' && Object.keys(config).length == 0) { if (f === 'partialUpdate' && Object.keys(config).length == 0) {
console.log('create form',Actions.CREATE?.form)
config = {...Actions.CREATE?.form, ...model.model_type?.['create']?.form, ...model?.['create']?.form} config = {...Actions.CREATE?.form, ...model.model_type?.['create']?.form, ...model?.['create']?.form}
config['title'] = {...action?.form_title, ...model.model_type?.[f]?.form_title, ...model?.[f]?.form_title} config['title'] = {...action?.form_title, ...model.model_type?.[f]?.form_title, ...model?.[f]?.form_title}
} }
@ -288,6 +285,7 @@ export function getForm(model, action, item1, item2) {
value = v value = v
} }
if (value?.form_field) { if (value?.form_field) {
value['value'] = item1?.[value?.field] ?? undefined
form.fields.push( form.fields.push(
{ {
...value, ...value,
@ -301,7 +299,6 @@ export function getForm(model, action, item1, item2) {
form[k] = value form[k] = value
} }
} }
console.log('utils form', form)
return form return form
} }
@ -374,7 +371,7 @@ export const CardMixin = {
let idx = undefined let idx = undefined
target = this.findCard(obj.id, card_list) target = this.findCard(obj.id, card_list)
if (target.parent) { if (target?.parent) {
let parent = this.findCard(target.parent, card_list) let parent = this.findCard(target.parent, card_list)
if (parent) { if (parent) {
if (parent.show_children){ if (parent.show_children){
@ -382,7 +379,7 @@ export const CardMixin = {
Vue.set(parent.children, idx, obj) Vue.set(parent.children, idx, obj)
} }
} }
} else { } else if (target) {
idx = card_list.indexOf(card_list.find(x => x.id === target.id)) idx = card_list.indexOf(card_list.find(x => x.id === target.id))
Vue.set(card_list, idx, obj) Vue.set(card_list, idx, obj)
} }

View File

@ -85,7 +85,7 @@ module.exports = {
}, },
}, },
// TODO make this conditional on .env DEBUG = FALSE // TODO make this conditional on .env DEBUG = FALSE
config.optimization.minimize(false) config.optimization.minimize(true)
); );
//TODO somehow remov them as they are also added to the manifest config of the service worker //TODO somehow remov them as they are also added to the manifest config of the service worker