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

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):
content = {'error': True, 'msg': _('An error occurred attempting to move ') + child.name}
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:
parent = self.model.objects.get(pk=parent, space=self.request.space)

View File

@ -3,131 +3,52 @@
<generic-modal-form
:model="this_model"
:action="this_action"
:item1="foods[5]"
:item2="undefined"
:show="true"/> <!-- TODO make this based on method -->
:item1="this_item"
:item2="this_target"
:show="show_modal"
@finish-action="finishAction"/>
<generic-split-lists
:list_name="this_model.name"
@reset="resetList"
@get-list="getFoods"
@get-list="getItems"
@item-action="startAction"
>
<template v-slot:cards-left>
<generic-horizontal-card
v-for="f in foods" v-bind:key="f.id"
:model=f
:model_name="this_model.name"
v-for="i in items_left" v-bind:key="i.id"
:item=i
:item_type="this_model.name"
:draggable="true"
:merge="true"
:move="true"
@item-action="startAction($event, 'left')"
>
<!-- foods can also be a recipe, show link to the recipe if it exists -->
<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"/>
<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="i.recipe.url"/>
</template>
</generic-horizontal-card>
</template>
<template v-slot:cards-right>
<generic-horizontal-card v-for="f in foods2" v-bind:key="f.id"
:model=f
:model_name="this_model.name"
<generic-horizontal-card v-for="i in items_right" v-bind:key="i.id"
:item=i
:item_type="this_model.name"
:draggable="true"
:merge="true"
:move="true"
@item-action="startAction($event, 'right')"
>
<!-- foods can also be a recipe, show link to the recipe if it exists -->
<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"/>
<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="i.recipe.url"/>
</template>
</generic-horizontal-card>
</template>
</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>
</template>
@ -145,90 +66,72 @@ import {StandardToasts} from "@/utils/utils";
import GenericSplitLists from "@/components/GenericSplitLists";
import GenericHorizontalCard from "@/components/GenericHorizontalCard";
import GenericMultiselect from "@/components/GenericMultiselect";
import GenericModalForm from "@/components/Modals/GenericModalForm";
Vue.use(BootstrapVue)
export default {
name: 'FoodListView',
name: 'FoodListView', // TODO: make generic name
mixins: [CardMixin, ToastMixin],
components: {GenericHorizontalCard, GenericMultiselect, GenericSplitLists, GenericModalForm},
components: {GenericHorizontalCard, GenericSplitLists, GenericModalForm},
data() {
return {
this_model: Models.FOOD, //TODO: mounted method to calcuate
this_action: Actions.UPDATE, //TODO: based on what we are doing
models: Models,
foods: [],
foods2: [],
items_left: [],
items_right: [],
load_more_left: true,
load_more_right: true,
blank_item: {
'id': undefined,
'name': '',
'description': '',
'recipe': null,
'recipe_full': undefined,
'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': ''
},
},
this_model: Models.FOOD, //TODO: mounted method to calcuate
this_action: undefined,
this_item: {},
this_target: {},
models: Models,
show_modal:false
}
},
methods: {
// TODO should model actions be included with the context menu? the card? a seperate mixin avaible to all?
resetList: function(e) {
if (e.column === 'left') {
this.foods = []
this.items_left = []
} else if (e.column === 'right') {
this.foods2 = []
this.items_right = []
}
},
startAction: function(e, param) {
let source = e?.source ?? this.blank_item
let source = e?.source ?? {}
let target = e?.target ?? undefined
this.this_item = source
this.this_item.target = target || undefined
this.this_target = target
switch (e.action) {
case 'delete':
this.$bvModal.show('id_modal_food_delete')
this.this_action = Actions.DELETE
this.show_modal = true
break;
case 'new':
this.this_item = {...this.blank_item}
this.$bvModal.show('id_modal_food_edit')
this.this_action = Actions.CREATE
this.show_modal = true
break;
case 'edit':
this.$bvModal.show('id_modal_food_edit')
this.this_item = e.source
this.this_action = Actions.UPDATE
this.show_modal = true
break;
case 'move':
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 {
this.moveFood(source.id, target.id)
this.moveThis(source.id, target.id)
}
break;
case 'merge':
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 {
this.mergeFood(e.source.id, e.target.id)
this.mergeThis(e.source.id, e.target.id)
}
break;
case 'get-children':
@ -247,25 +150,51 @@ export default {
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'
// TODO: does this need to be a callback?
genericAPI(this.this_model, Actions.LIST, params).then((result) => {
if (result.data.results.length){
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') {
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
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 {
callback(false) // stop loading
console.log('no data returned')
}
// 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) => {
console.log(err)
@ -275,50 +204,52 @@ export default {
getThis: function(id, callback){
return genericAPI(this.this_model, Actions.FETCH, {'id': id})
},
saveFood: function () {
let food = {...this.this_item}
food.supermarket_category = this.this_item.supermarket_category?.id ?? null
food.recipe = this.this_item.recipe?.id ?? null
if (!food?.id) { // if there is no item id assume it's a new item
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)
saveThis: function (thisItem) {
if (!thisItem?.id) { // if there is no item id assume it's a new item
genericAPI(this.this_model, Actions.CREATE, thisItem).then((result) => {
// place all new items at the top of the list - could sort instead
this.items_left = [result.data].concat(this.items_left)
// this creates a deep copy to make sure that columns stay independent
if (this.show_split){
this.foods2 = [...this.foods]
} else {
this.foods2 = []
}
this.items_right = [{...result.data}].concat(this.items_right)
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
}).catch((err) => {
console.log(err)
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
})
} else {
genericAPI(this.this_model, Actions.UPDATE, food).then((result) => {
this.refreshObject(food.id)
genericAPI(this.this_model, Actions.UPDATE, thisItem).then((result) => {
this.refreshThis(thisItem.id)
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE)
}).catch((err) => {
console.log(err)
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) => {
if (target_id === 0) {
let food = this.findCard(source_id, this.foods) || this.findCard(source_id, this.foods2)
this.foods = [food].concat(this.destroyCard(source_id, this.foods)) // 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
food.parent = null
let item = this.findCard(source_id, this.items_left) || this.findCard(source_id, this.items_right)
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.items_right = [...[item]].concat(this.destroyCard(source_id, this.items_right)) // order matters, destroy old card before adding it back in at root
item.parent = null
} else {
this.foods = this.destroyCard(source_id, this.foods)
this.foods2 = this.destroyCard(source_id, this.foods2)
this.refreshObject(target_id)
this.items_left = this.destroyCard(source_id, this.items_left)
this.items_right = this.destroyCard(source_id, this.items_right)
this.refreshThis(target_id)
}
// TODO make standard toast
this.makeToast(this.$t('Success'), 'Succesfully moved food', 'success')
this.makeToast(this.$t('Success'), 'Succesfully moved resource', 'success')
}).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
@ -326,26 +257,38 @@ export default {
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) => {
this.foods = this.destroyCard(source_id, this.foods)
this.foods2 = this.destroyCard(source_id, this.foods2)
this.refreshObject(target_id)
this.items_left = this.destroyCard(source_id, this.items_left)
this.items_right = this.destroyCard(source_id, this.items_right)
this.refreshThis(target_id)
// TODO make standard toast
this.makeToast(this.$t('Success'), 'Succesfully merged resource', 'success')
}).catch((err) => {
//TODO error checking not working with OpenAPI methods
console.log('Error', err)
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 options = {
'root': food.id,
'root': item.id,
'pageSize': 200
}
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) {
Vue.set(parent, 'children', result.data.results)
Vue.set(parent, 'show_children', true)
@ -358,13 +301,13 @@ export default {
},
getRecipes: function(col, food){
let parent = {}
// TODO: make this generic
let options = {
'foods': food.id,
'pageSize': 200
}
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) {
Vue.set(parent, 'recipes', result.data.results)
Vue.set(parent, 'show_recipes', true)
@ -376,32 +319,28 @@ export default {
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
})
},
refreshObject: function(id){
refreshThis: function(id){
this.getThis(id).then(result => {
this.refreshCard(result.data, this.foods)
this.refreshCard({...result.data}, this.foods2)
this.refreshCard(result.data, this.items_left)
this.refreshCard({...result.data}, this.items_right)
})
},
// 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
},
deleteThis: function(id, model) {
deleteThis: function(id) {
genericAPI(this.this_model, Actions.DELETE, {'id': id}).then((result) => {
this.foods = this.destroyCard(id, this.foods)
this.foods2 = this.destroyCard(id, this.foods2)
this.items_left = this.destroyCard(id, this.items_left)
this.items_right = this.destroyCard(id, this.items_right)
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_DELETE)
}).catch((err) => {
console.log(err)
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)">
<b-row no-gutters style="height:inherit;">
<b-col no-gutters md="3" style="height:inherit;">
<b-card-img-lazy style="object-fit: cover; height: 10vh;" :src="model_image" v-bind:alt="$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 no-gutters md="9" 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">
<h5 class="m-0 mt-1 text-truncate">{{ model[title] }}</h5>
<div class= "m-0 text-truncate">{{ model[subtitle] }}</div>
<h5 class="m-0 mt-1 text-truncate">{{ item[title] }}</h5>
<div class= "m-0 text-truncate">{{ item[subtitle] }}</div>
<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"
style="z-index: 800;" v-on:click="$emit('item-action',{'action':'get-children','source':model})">
<div v-if="!model.show_children">{{ model[child_count] }} {{ model_name }}</div>
<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':item})">
<div v-if="!item.show_children">{{ item[child_count] }} {{ item_type }}</div>
<div v-else>{{ text.hide_children }}</div>
</div>
<div v-if="model[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})">
<div v-if="!model.show_recipes">{{ model[recipe_count] }} {{$t('Recipes')}}</div>
<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':item})">
<div v-if="!item.show_recipes">{{ item[recipe_count] }} {{$t('Recipes')}}</div>
<div v-else>{{$t('Hide_Recipes')}}</div>
</div>
</div>
@ -38,18 +38,18 @@
<generic-context-menu class="p-0"
:show_merge="merge"
:show_move="move"
@item-action="$emit('item-action', {'action': $event, 'source': model})">
@item-action="$emit('item-action', {'action': $event, 'source': item})">
</generic-context-menu>
</div>
</b-row>
</b-card>
<!-- 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">
<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"
:model="child"
:model_name="model_name"
:item="child"
:item_type="item_type"
:title="title"
:subtitle="subtitle"
:child_count="child_count"
@ -63,10 +63,10 @@
</div>
</div>
<!-- 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 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"
:recipe="r">
</recipe-card>
@ -75,11 +75,11 @@
</div>
<!-- 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-item v-if="move" action v-on:click="$emit('item-action',{'action': 'move', 'target': model, 'source': source}); closeMenu()">
{{$t('Move')}}: {{$t('move_confirmation', {'child': source.name,'parent':model.name})}}
<b-list-group-item v-if="move" action v-on:click="$emit('item-action',{'action': 'move', 'target': item, 'source': source}); closeMenu()">
<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 v-if="merge" action v-on:click="$emit('item-action',{'action': 'merge', 'target': model, 'source': source}); closeMenu()">
{{$t('Merge')}}: {{ $t('merge_confirmation', {'source': source.name,'target':model.name}) }}
<b-list-group-item v-if="merge" action v-on:click="$emit('item-action',{'action': 'merge', 'target': item, 'source': source}); closeMenu()">
<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 action v-on:click="closeMenu()">
{{$t('Cancel')}}
@ -101,8 +101,8 @@ export default {
components: { GenericContextMenu, RecipeCard },
mixins: [clickaway],
props: {
model: Object,
model_name: {type: String, default: 'Blank Model'}, // TODO update translations to handle plural translations
item: Object,
item_type: {type: String, default: 'Blank Item Type'}, // TODO update translations to handle plural translations
draggable: {type: Boolean, default: false},
title: {type: String, default: 'name'},
subtitle: {type: String, default: 'description'},
@ -115,7 +115,7 @@ export default {
},
data() {
return {
model_image: '',
item_image: '',
over: false,
show_menu: false,
dragMenu: undefined,
@ -128,14 +128,14 @@ export default {
}
},
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.text.hide_children = this.$t('Hide_' + this.model_name)
this.text.hide_children = this.$t('Hide_' + this.item_type)
},
methods: {
handleDragStart: function(e) {
this.isError = false
e.dataTransfer.setData('source', JSON.stringify(this.model))
e.dataTransfer.setData('source', JSON.stringify(this.item))
},
handleDragEnter: function(e) {
if (!e.currentTarget.contains(e.relatedTarget) && e.relatedTarget != null) {
@ -149,7 +149,7 @@ export default {
},
handleDragDrop: function(e) {
let source = JSON.parse(e.dataTransfer.getData('source'))
if (source.id != this.model.id){
if (source.id != this.item.id){
this.source = source
let menuLocation = {getBoundingClientRect: this.generateLocation(e.clientX, e.clientY),}
this.show_menu = true
@ -176,7 +176,7 @@ export default {
})
popper.update()
this.over = false
this.$emit({'action': 'drop', 'target': this.model, 'source': this.source})
this.$emit({'action': 'drop', 'target': this.item, 'source': this.source})
} else {
this.isError = true
}

View File

@ -76,7 +76,7 @@
<template v-slot:no-more><span/></template>
</infinite-loading>
</div>
<!-- right side food cards -->
<!-- right side cards -->
<div class="col col-md mh-100 overflow-auto" v-if="show_split">
<slot name="cards-right"></slot>
<infinite-loading
@ -119,8 +119,6 @@ export default {
left_page: 0,
right: +new Date(),
left: +new Date(),
isDirtyright: false,
isDirtyleft: false,
text: {
'new': '',
'name': '',
@ -133,7 +131,6 @@ export default {
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() {
@ -170,6 +167,7 @@ export default {
'page': (col==='left') ? this.left_page + 1 : this.right_page + 1,
'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) => {
this[col+'_page']+=1
$state.loaded();

View File

@ -1,6 +1,6 @@
<template>
<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>
<div v-for="(f, i) in form.fields" v-bind:key=i>
<p v-if="f.type=='instruction'">{{f.label}}</p>
@ -10,7 +10,8 @@
:field="f.field"
:model="listModel(f.list)"
: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'"
:label="f.label"
:value="f.value"
@ -23,11 +24,10 @@
</div>
<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>
</template>
</b-modal>
<b-button v-on:click="Button">ok</b-button>
</div>
</template>
@ -55,19 +55,13 @@ export default {
},
data() {
return {
new_item: {},
form_data: {},
form: {},
buttons: {
'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')}
}
dirty: false
}
},
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: {
buttonLabel() {
@ -78,30 +72,27 @@ export default {
'show': function () {
if (this.show) {
this.form = getForm(this.model, this.action, this.item1, this.item2)
this.dirty = true
this.$bvModal.show('modal')
} else {
this.$bvModal.hide('modal')
this.form_data = {}
}
},
},
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(){
let alert_text = ''
for (const [k, v] of Object.entries(this.form.fields)) {
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)
this.dirty = false
this.$emit('finish-action', {'form_data': this.form_data })
},
changeValue: function(field, value) {
// console.log('catch change', field, value)
this.new_item[field] = value
cancelAction: function() {
if (this.dirty) {
this.dirty = false
this.$emit('finish-action', 'cancel')
}
},
storeValue: function(field, value) {
this.form_data[field] = value
},
listModel: function(m) {
if (m === 'self') {

View File

@ -120,5 +120,6 @@
"edit_title": "Edit {type}",
"Name": "Name",
"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')
break;
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;
case StandardToasts.FAIL_FETCH:
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 params = setup?.params ?? []
let parameters = []
let this_value = undefined
params.forEach(function (item, index) {
if (Array.isArray(item)) {
@ -181,8 +180,7 @@ export function genericAPI(model, action, options) {
}
}
} else {
this_value = options?.[item] ?? undefined
if (this_value) {this_value = formatParam(config?.[item], this_value)}
this_value = formatParam(config?.[item], options?.[item] ?? undefined)
}
// if no value is found so far, get the default if it exists
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}
// if not defined partialUpdate will use form from create
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['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
}
if (value?.form_field) {
value['value'] = item1?.[value?.field] ?? undefined
form.fields.push(
{
...value,
@ -301,7 +299,6 @@ export function getForm(model, action, item1, item2) {
form[k] = value
}
}
console.log('utils form', form)
return form
}
@ -374,7 +371,7 @@ export const CardMixin = {
let idx = undefined
target = this.findCard(obj.id, card_list)
if (target.parent) {
if (target?.parent) {
let parent = this.findCard(target.parent, card_list)
if (parent) {
if (parent.show_children){
@ -382,7 +379,7 @@ export const CardMixin = {
Vue.set(parent.children, idx, obj)
}
}
} else {
} else if (target) {
idx = card_list.indexOf(card_list.find(x => x.id === target.id))
Vue.set(card_list, idx, obj)
}

View File

@ -85,7 +85,7 @@ module.exports = {
},
},
// 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