generic modal refactored

This commit is contained in:
smilerz
2021-11-23 19:18:10 -06:00
parent 3fe5340592
commit 5b6dd62f8e
17 changed files with 1951 additions and 1805 deletions

View File

@@ -1,479 +1,477 @@
<template>
<div id="app" style="margin-bottom: 4vh" v-if="this_model">
<generic-modal-form v-if="this_model"
:model="this_model"
:action="this_action"
:item1="this_item"
:item2="this_target"
:show="show_modal"
@finish-action="finishAction"/>
<div id="app" style="margin-bottom: 4vh" v-if="this_model">
<generic-modal-form v-if="this_model" :model="this_model" :action="this_action" :item1="this_item" :item2="this_target" :show="show_modal" @finish-action="finishAction" />
<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">
<!-- dynamically loaded header components -->
<div class="row" v-if="header_component_name !== ''">
<div class="col-md-12">
<component :is="headerComponent"></component>
</div>
</div>
<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">
<div class="row">
<div class="col-md-9" style="margin-top: 1vh">
<h3>
<!-- <span><b-button variant="link" size="sm" class="text-dark shadow-none"><i class="fas fa-chevron-down"></i></b-button></span> -->
<model-menu />
<span>{{ this.this_model.name }}</span>
<span v-if="this_model.name !== 'Step'"
><b-button variant="link" @click="startAction({ action: 'new' })"><i class="fas fa-plus-circle fa-2x"></i></b-button></span
><!-- TODO add proper field to model config to determine if create should be available or not -->
</h3>
</div>
<div class="col-md-3" style="position: relative; margin-top: 1vh">
<b-form-checkbox
v-model="show_split"
name="check-button"
v-if="paginated"
class="shadow-none"
style="position: relative; top: 50%; transform: translateY(-50%)"
switch
>
{{ $t("show_split_screen") }}
</b-form-checkbox>
</div>
</div>
<!-- dynamically loaded header components -->
<div class="row" v-if="header_component_name !== ''">
<div class="col-md-12">
<component :is="headerComponent"></component>
<div class="row">
<div class="col" :class="{ 'col-md-6': show_split }">
<!-- model isn't paginated and loads in one API call -->
<div v-if="!paginated">
<generic-horizontal-card
v-for="i in items_left"
v-bind:key="i.id"
:item="i"
:model="this_model"
@item-action="startAction($event, 'left')"
@finish-action="finishAction"
/>
</div>
<!-- model is paginated and needs managed -->
<generic-infinite-cards v-if="paginated" :card_counts="left_counts" :scroll="show_split" @search="getItems($event, 'left')" @reset="resetList('left')">
<template v-slot:cards>
<generic-horizontal-card
v-for="i in items_left"
v-bind:key="i.id"
:item="i"
:model="this_model"
@item-action="startAction($event, 'left')"
@finish-action="finishAction"
/>
</template>
</generic-infinite-cards>
</div>
<div class="col col-md-6" v-if="show_split">
<generic-infinite-cards
v-if="this_model"
:card_counts="right_counts"
:scroll="show_split"
@search="getItems($event, 'right')"
@reset="resetList('right')"
>
<template v-slot:cards>
<generic-horizontal-card
v-for="i in items_right"
v-bind:key="i.id"
:item="i"
:model="this_model"
@item-action="startAction($event, 'right')"
@finish-action="finishAction"
/>
</template>
</generic-infinite-cards>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-9" style="margin-top: 1vh">
<h3>
<!-- <span><b-button variant="link" size="sm" class="text-dark shadow-none"><i class="fas fa-chevron-down"></i></b-button></span> -->
<model-menu/>
<span>{{ this.this_model.name }}</span>
<span v-if="this_model.name !== 'Step'"><b-button variant="link" @click="startAction({'action':'new'})"><i
class="fas fa-plus-circle fa-2x"></i></b-button></span><!-- TODO add proper field to model config to determine if create should be available or not -->
</h3>
</div>
<div class="col-md-3" style="position: relative; margin-top: 1vh">
<b-form-checkbox v-model="show_split" name="check-button" v-if="paginated"
class="shadow-none"
style="position:relative;top: 50%; transform: translateY(-50%);" switch>
{{ $t('show_split_screen') }}
</b-form-checkbox>
</div>
</div>
<div class="row">
<div class="col" :class="{'col-md-6' : show_split}">
<!-- model isn't paginated and loads in one API call -->
<div v-if="!paginated">
<generic-horizontal-card v-for="i in items_left" v-bind:key="i.id"
:item=i
:model="this_model"
@item-action="startAction($event, 'left')"
@finish-action="finishAction"/>
</div>
<!-- model is paginated and needs managed -->
<generic-infinite-cards v-if="paginated"
:card_counts="left_counts"
:scroll="show_split"
@search="getItems($event, 'left')"
@reset="resetList('left')">
<template v-slot:cards>
<generic-horizontal-card
v-for="i in items_left" v-bind:key="i.id"
:item=i
:model="this_model"
@item-action="startAction($event, 'left')"
@finish-action="finishAction"/>
</template>
</generic-infinite-cards>
</div>
<div class="col col-md-6" v-if="show_split">
<generic-infinite-cards v-if="this_model"
:card_counts="right_counts"
:scroll="show_split"
@search="getItems($event, 'right')"
@reset="resetList('right')">
<template v-slot:cards>
<generic-horizontal-card
v-for="i in items_right" v-bind:key="i.id"
:item=i
:model="this_model"
@item-action="startAction($event, 'right')"
@finish-action="finishAction"/>
</template>
</generic-infinite-cards>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import Vue from "vue"
import { BootstrapVue } from "bootstrap-vue"
import Vue from 'vue'
import {BootstrapVue} from 'bootstrap-vue'
import "bootstrap-vue/dist/bootstrap-vue.css"
import 'bootstrap-vue/dist/bootstrap-vue.css'
import { CardMixin, ApiMixin, getConfig } from "@/utils/utils"
import { StandardToasts, ToastMixin } from "@/utils/utils"
import {CardMixin, ApiMixin, getConfig} from "@/utils/utils";
import {StandardToasts, ToastMixin} from "@/utils/utils";
import GenericInfiniteCards from "@/components/GenericInfiniteCards";
import GenericHorizontalCard from "@/components/GenericHorizontalCard";
import GenericModalForm from "@/components/Modals/GenericModalForm";
import ModelMenu from "@/components/ModelMenu";
import {ApiApiFactory} from "@/utils/openapi/api";
import GenericInfiniteCards from "@/components/GenericInfiniteCards"
import GenericHorizontalCard from "@/components/GenericHorizontalCard"
import GenericModalForm from "@/components/Modals/GenericModalForm"
import ModelMenu from "@/components/ModelMenu"
import { ApiApiFactory } from "@/utils/openapi/api"
//import StorageQuota from "@/components/StorageQuota";
Vue.use(BootstrapVue)
export default {
// TODO ApiGenerator doesn't capture and share error information - would be nice to share error details when available
// or i'm capturing it incorrectly
name: 'ModelListView',
mixins: [CardMixin, ApiMixin, ToastMixin],
components: {
GenericHorizontalCard, GenericModalForm, GenericInfiniteCards, ModelMenu,
},
data() {
return {
// this.Models and this.Actions inherited from ApiMixin
items_left: [],
items_right: [],
right_counts: {'max': 9999, 'current': 0},
left_counts: {'max': 9999, 'current': 0},
this_model: undefined,
model_menu: undefined,
this_action: undefined,
this_recipe_param: undefined,
this_item: {},
this_target: {},
show_modal: false,
show_split: false,
paginated: false,
header_component_name: undefined,
}
},
computed: {
headerComponent() {
// TODO this leads webpack to create one .js file for each component in this folder because at runtime any one of them could be requested
// TODO this is not necessarily bad but maybe there are better options to do this
return () => import(/* webpackChunkName: "header-component" */ `@/components/${this.header_component_name}`)
}
},
mounted() {
// value is passed from lists.py
let model_config = JSON.parse(document.getElementById('model_config').textContent)
this.this_model = this.Models[model_config?.model]
this.this_recipe_param = model_config?.recipe_param
this.paginated = this.this_model?.paginated ?? false
this.header_component_name = this.this_model?.list?.header_component?.name ?? undefined
this.$nextTick(() => {
if (!this.paginated) {
this.getItems({page:1},'left')
}
})
this.$i18n.locale = window.CUSTOM_LOCALE
},
methods: {
// this.genericAPI inherited from ApiMixin
resetList: function (e) {
this['items_' + e] = []
this[e + '_counts'].max = 9999 + Math.random()
this[e + '_counts'].current = 0
// TODO ApiGenerator doesn't capture and share error information - would be nice to share error details when available
// or i'm capturing it incorrectly
name: "ModelListView",
mixins: [CardMixin, ApiMixin, ToastMixin],
components: {
GenericHorizontalCard,
GenericModalForm,
GenericInfiniteCards,
ModelMenu,
},
startAction: function (e, param) {
let source = e?.source ?? {}
let target = e?.target ?? undefined
this.this_item = source
this.this_target = target
switch (e.action) {
case 'delete':
this.this_action = this.Actions.DELETE
this.show_modal = true
break;
case 'new':
this.this_action = this.Actions.CREATE
this.show_modal = true
break;
case 'edit':
this.this_item = e.source
this.this_action = this.Actions.UPDATE
this.show_modal = true
break;
case 'move':
if (target == null) {
this.this_item = e.source
this.this_action = this.Actions.MOVE
this.show_modal = true
} else {
this.moveThis(source.id, target.id)
}
break;
case 'merge':
if (target == null) {
this.this_item = e.source
this.this_action = this.Actions.MERGE
this.show_modal = true
} else {
this.mergeThis(e.source, e.target, false)
}
break;
case 'merge-automate':
if (target == null) {
this.this_item = e.source
this.this_action = this.Actions.MERGE
this.show_modal = true
} else {
this.mergeThis(e.source, e.target, true)
}
break
case 'get-children':
if (source.show_children) {
Vue.set(source, 'show_children', false)
} else {
this.getChildren(param, source)
}
break;
case 'get-recipes':
if (source.show_recipes) {
Vue.set(source, 'show_recipes', false)
} else {
this.getRecipes(param, source)
}
break;
}
},
finishAction: function (e) {
let update = undefined
switch (e?.action) {
case 'save':
this.saveThis(e.form_data)
break;
}
if (e !== 'cancel') {
switch (this.this_action) {
case this.Actions.DELETE:
this.deleteThis(this.this_item.id)
break;
case this.Actions.CREATE:
this.saveThis(e.form_data)
break;
case this.Actions.UPDATE:
update = e.form_data
update.id = this.this_item.id
this.saveThis(update)
break;
case this.Actions.MERGE:
this.mergeThis(this.this_item, e.form_data.target, false)
break;
case this.Actions.MOVE:
this.moveThis(this.this_item.id, e.form_data.target.id)
break;
data() {
return {
// this.Models and this.Actions inherited from ApiMixin
items_left: [],
items_right: [],
right_counts: { max: 9999, current: 0 },
left_counts: { max: 9999, current: 0 },
this_model: undefined,
model_menu: undefined,
this_action: undefined,
this_recipe_param: undefined,
this_item: {},
this_target: {},
show_modal: false,
show_split: false,
paginated: false,
header_component_name: undefined,
}
}
this.clearState()
},
getItems: function (params, col) {
let column = col || 'left'
params.options = {'query':{'extended': 1}} // returns extended values in API response
this.genericAPI(this.this_model, this.Actions.LIST, params).then((result) => {
let results = result.data?.results ?? result.data
if (results?.length) {
// let secondaryRequest = undefined;
// if (this['items_' + column]?.length < getConfig(this.this_model, this.Actions.LIST).config.pageSize.default * (params.page - 1)) {
// // the item list is smaller than it should be based on the site the user is own
// // this happens when an item is deleted (or merged)
// // to prevent issues insert the last item of the previous search page before loading the new results
// params.page = params.page - 1
// secondaryRequest = this.genericAPI(this.this_model, this.Actions.LIST, params).then((result) => {
// let prev_page_results = result.data?.results ?? result.data
// if (prev_page_results?.length) {
// results = [prev_page_results[prev_page_results.length]].concat(results)
//
// this['items_' + column] = this['items_' + column].concat(results) //TODO duplicate code, find some elegant workaround
// this[column + '_counts']['current'] = getConfig(this.this_model, this.Actions.LIST).config.pageSize.default * (params.page - 1) + results.length
// this[column + '_counts']['max'] = result.data?.count ?? 0
// }
// })
// } else {
//
// }
this['items_' + column] = this['items_' + column].concat(results)
this[column + '_counts']['current'] = getConfig(this.this_model, this.Actions.LIST).config.pageSize.default * (params.page - 1) + results.length
this[column + '_counts']['max'] = result.data?.count ?? 0
} else {
this[column + '_counts']['max'] = 0
this[column + '_counts']['current'] = 0
console.log('no data returned')
}
}).catch((err) => {
console.log(err, Object.keys(err))
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
})
computed: {
headerComponent() {
// TODO this leads webpack to create one .js file for each component in this folder because at runtime any one of them could be requested
// TODO this is not necessarily bad but maybe there are better options to do this
return () => import(/* webpackChunkName: "header-component" */ `@/components/${this.header_component_name}`)
},
},
getThis: function (id, callback) {
return this.genericAPI(this.this_model, this.Actions.FETCH, {'id': id})
},
saveThis: function (thisItem) {
if (!thisItem?.id) { // if there is no item id assume it's a new item
this.genericAPI(this.this_model, this.Actions.CREATE, thisItem).then((result) => {
// look for and destroy any existing cards to prevent duplicates in the GET case of get_or_create
// then place all new items at the top of the list - could sort instead
this.items_left = [result.data].concat(this.destroyCard(result?.data?.id, this.items_left))
// this creates a deep copy to make sure that columns stay independent
this.items_right = [{...result.data}].concat(this.destroyCard(result?.data?.id, this.items_right))
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
}).catch((err) => {
console.log(err)
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
mounted() {
// value is passed from lists.py
let model_config = JSON.parse(document.getElementById("model_config").textContent)
this.this_model = this.Models[model_config?.model]
this.this_recipe_param = model_config?.recipe_param
this.paginated = this.this_model?.paginated ?? false
this.header_component_name = this.this_model?.list?.header_component?.name ?? undefined
this.$nextTick(() => {
if (!this.paginated) {
this.getItems({ page: 1 }, "left")
}
})
} else {
this.genericAPI(this.this_model, this.Actions.UPDATE, thisItem).then((result) => {
this.refreshThis(thisItem.id)
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE)
}).catch((err) => {
console.log(err, err.response)
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
})
}
this.$i18n.locale = window.CUSTOM_LOCALE
},
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
}
let item = this.findCard(source_id, this.items_left) || this.findCard(source_id, this.items_right)
if (source_id === undefined || target_id === undefined || item?.parent == target_id) {
this.makeToast(this.$t('Warning'), this.$t('Nothing to do'), 'warning')
this.clearState()
return
}
this.genericAPI(this.this_model, this.Actions.MOVE, {'source': source_id, 'target': target_id}).then((result) => {
if (target_id === 0) {
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.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 resource', 'success')
}).catch((err) => {
console.log(err)
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
})
},
mergeThis: function (source, target, automate) {
let source_id = source.id
let target_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
}
this.genericAPI(this.this_model, this.Actions.MERGE, {
'source': source_id,
'target': target_id
}).then((result) => {
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')
})
methods: {
// this.genericAPI inherited from ApiMixin
resetList: function (e) {
this["items_" + e] = []
this[e + "_counts"].max = 9999 + Math.random()
this[e + "_counts"].current = 0
},
startAction: function (e, param) {
let source = e?.source ?? {}
let target = e?.target ?? undefined
this.this_item = source
this.this_target = target
if (automate) {
let apiClient = new ApiApiFactory()
switch (e.action) {
case "delete":
this.this_action = this.Actions.DELETE
this.show_modal = true
break
case "new":
this.this_action = this.Actions.CREATE
this.show_modal = true
break
case "edit":
this.this_item = e.source
this.this_action = this.Actions.UPDATE
this.show_modal = true
break
case "move":
if (target == null) {
this.this_item = e.source
this.this_action = this.Actions.MOVE
this.show_modal = true
} else {
// this is redundant - function also exists in GenericModal
this.moveThis(source.id, target.id)
}
break
case "merge":
if (target == null) {
this.this_item = e.source
this.this_action = this.Actions.MERGE
this.show_modal = true
} else {
// this is redundant - function also exists in GenericModal
this.mergeThis(e.source, e.target, false)
}
break
case "merge-automate":
if (target == null) {
this.this_item = e.source
this.this_action = this.Actions.MERGE
this.this_item.automate = true
this.show_modal = true
} else {
// this is redundant - function also exists in GenericModal
this.mergeThis(e.source, e.target, true)
}
break
case "get-children":
if (source.show_children) {
Vue.set(source, "show_children", false)
} else {
this.getChildren(param, source)
}
break
case "get-recipes":
if (source.show_recipes) {
Vue.set(source, "show_recipes", false)
} else {
this.getRecipes(param, source)
}
break
}
},
finishAction: function (e) {
switch (e?.action) {
case "save":
this.saveThis(e.form_data)
break
}
if (e !== "cancel") {
switch (this.this_action) {
case this.Actions.DELETE:
console.log("delete")
this.deleteThis(this.this_item.id)
break
case this.Actions.CREATE:
this.saveThis(e.item)
break
case this.Actions.UPDATE:
this.updateThis(this.this_item)
break
case this.Actions.MERGE:
this.mergeUpdateItem(this.this_item.id, e.target)
break
case this.Actions.MOVE:
this.moveUpdateItem(this.this_item.id, e.target)
break
}
}
this.clearState()
},
getItems: function (params, col) {
let column = col || "left"
params.options = { query: { extended: 1 } } // returns extended values in API response
this.genericAPI(this.this_model, this.Actions.LIST, params)
.then((result) => {
let results = result.data?.results ?? result.data
let automation = {
name: `Merge ${source.name} with ${target.name}`,
param_1: source.name,
param_2: target.name
}
if (results?.length) {
// let secondaryRequest = undefined;
// if (this['items_' + column]?.length < getConfig(this.this_model, this.Actions.LIST).config.pageSize.default * (params.page - 1)) {
// // the item list is smaller than it should be based on the site the user is own
// // this happens when an item is deleted (or merged)
// // to prevent issues insert the last item of the previous search page before loading the new results
// params.page = params.page - 1
// secondaryRequest = this.genericAPI(this.this_model, this.Actions.LIST, params).then((result) => {
// let prev_page_results = result.data?.results ?? result.data
// if (prev_page_results?.length) {
// results = [prev_page_results[prev_page_results.length]].concat(results)
//
// this['items_' + column] = this['items_' + column].concat(results) //TODO duplicate code, find some elegant workaround
// this[column + '_counts']['current'] = getConfig(this.this_model, this.Actions.LIST).config.pageSize.default * (params.page - 1) + results.length
// this[column + '_counts']['max'] = result.data?.count ?? 0
// }
// })
// } else {
//
// }
if (this.this_model === this.Models.FOOD) {
automation.type = 'FOOD_ALIAS'
}
if (this.this_model === this.Models.UNIT) {
automation.type = 'UNIT_ALIAS'
}
if (this.this_model === this.Models.KEYWORD) {
automation.type = 'KEYWORD_ALIAS'
}
this["items_" + column] = this["items_" + column].concat(results)
this[column + "_counts"]["current"] = getConfig(this.this_model, this.Actions.LIST).config.pageSize.default * (params.page - 1) + results.length
this[column + "_counts"]["max"] = result.data?.count ?? 0
} else {
this[column + "_counts"]["max"] = 0
this[column + "_counts"]["current"] = 0
console.log("no data returned")
}
})
.catch((err) => {
console.log(err, Object.keys(err))
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
})
},
getThis: function (id, callback) {
return this.genericAPI(this.this_model, this.Actions.FETCH, { id: id })
},
saveThis: function (item) {
// look for and destroy any existing cards to prevent duplicates in the GET case of get_or_create
// then place all new items at the top of the list - could sort instead
this.items_left = [item].concat(this.destroyCard(item?.id, this.items_left))
// this creates a deep copy to make sure that columns stay independent
this.items_right = [{ ...item }].concat(this.destroyCard(item?.id, this.items_right))
},
updateThis: function (item) {
this.refreshThis(item.id)
},
moveThis: function (source_id, target_id) {
// TODO: this function is almost 100% duplicated in GenericModalForm and only exists to enable drag and drop
if (source_id === target_id) {
this.makeToast(this.$t("Error"), this.$t("err_move_self"), "danger")
this.clearState()
return
}
let item = this.findCard(source_id, this.items_left) || this.findCard(source_id, this.items_right)
if (source_id === undefined || target_id === undefined || item?.parent == target_id) {
this.makeToast(this.$t("Warning"), this.$t("nothing"), "warning")
this.clearState()
return
}
this.genericAPI(this.this_model, this.Actions.MOVE, { source: source_id, target: target_id })
.then((result) => {
this.moveUpdateItem(source_id, target_id)
// TODO make standard toast
this.makeToast(this.$t("Success"), "Succesfully moved resource", "success")
})
.catch((err) => {
console.log(err)
this.makeToast(this.$t("Error"), err.bodyText, "danger")
})
},
moveUpdateItem: function (source_id, target_id) {
let item = this.findCard(source_id, this.items_left) || this.findCard(source_id, this.items_right)
if (target_id === 0) {
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.items_left = this.destroyCard(source_id, this.items_left)
this.items_right = this.destroyCard(source_id, this.items_right)
this.refreshThis(target_id)
}
},
mergeThis: function (source, target, automate) {
// TODO: this function is almost 100% duplicated in GenericModalForm and only exists to enable drag and drop
let source_id = source.id
let target_id = target.id
if (source_id === target_id) {
this.makeToast(this.$t("Error"), this.$t("err_merge_self"), "danger")
this.clearState()
return
}
if (!source_id || !target_id) {
this.makeToast(this.$t("Warning"), this.$t("nothing"), "warning")
this.clearState()
return
}
this.genericAPI(this.this_model, this.Actions.MERGE, {
source: source_id,
target: target_id,
})
.then((result) => {
this.mergeUpdateItem(source_id, 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")
})
apiClient.createAutomation(automation)
}
if (automate) {
let apiClient = new ApiApiFactory()
},
getChildren: function (col, item) {
let parent = {}
let params = {
'root': item.id,
'pageSize': 200,
'query': {'extended': 1},
'options': {'query':{'extended': 1}}
}
this.genericAPI(this.this_model, this.Actions.LIST, params).then((result) => {
parent = this.findCard(item.id, this['items_' + col])
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, item) {
let parent = {}
// TODO: make this generic
let params = {'pageSize': 50}
params[this.this_recipe_param] = item.id
console.log('RECIPE PARAM', this.this_recipe_param, params, item.id)
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params).then((result) => {
parent = this.findCard(item.id, this['items_' + col])
if (parent) {
Vue.set(parent, 'recipes', result.data.results)
Vue.set(parent, 'show_recipes', true)
Vue.set(parent, 'show_children', false)
}
let automation = {
name: `Merge ${source.name} with ${target.name}`,
param_1: source.name,
param_2: target.name,
}
}).catch((err) => {
console.log(err)
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
})
if (this.this_model === this.Models.FOOD) {
automation.type = "FOOD_ALIAS"
}
if (this.this_model === this.Models.UNIT) {
automation.type = "UNIT_ALIAS"
}
if (this.this_model === this.Models.KEYWORD) {
automation.type = "KEYWORD_ALIAS"
}
apiClient.createAutomation(automation)
}
},
mergeUpdateItem: function (source, target, automate) {
this.items_left = this.destroyCard(source, this.items_left)
this.items_right = this.destroyCard(source, this.items_right)
this.refreshThis(target)
},
getChildren: function (col, item) {
let parent = {}
let params = {
root: item.id,
pageSize: 200,
query: { extended: 1 },
options: { query: { extended: 1 } },
}
this.genericAPI(this.this_model, this.Actions.LIST, params)
.then((result) => {
parent = this.findCard(item.id, this["items_" + col])
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, item) {
let parent = {}
// TODO: make this generic
let params = { pageSize: 50 }
params[this.this_recipe_param] = item.id
console.log("RECIPE PARAM", this.this_recipe_param, params, item.id)
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params)
.then((result) => {
parent = this.findCard(item.id, this["items_" + col])
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")
})
},
refreshThis: function (id) {
this.getThis(id).then((result) => {
this.refreshCard(result.data, this.items_left)
this.refreshCard({ ...result.data }, this.items_right)
})
},
deleteThis: function (id) {
this.items_left = this.destroyCard(id, this.items_left)
this.items_right = this.destroyCard(id, this.items_right)
},
clearState: function () {
this.show_modal = false
this.this_action = undefined
this.this_item = undefined
this.this_target = undefined
},
},
refreshThis: function (id) {
this.getThis(id).then(result => {
this.refreshCard(result.data, this.items_left)
this.refreshCard({...result.data}, this.items_right)
})
},
deleteThis: function (id) {
this.genericAPI(this.this_model, this.Actions.DELETE, {'id': id}).then((result) => {
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
}
}
}
</script>
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
<style>
</style>
<style></style>

View File

@@ -1,44 +1,45 @@
<template>
<div v-if="itemList">
<span :key="k.id" v-for="k in itemList" class="pl-1">
<b-badge pill :variant="color">{{thisLabel(k)}}</b-badge>
</span>
<span :key="k.id" v-for="k in itemList" class="pl-1">
<b-badge pill :variant="color">{{ thisLabel(k) }}</b-badge>
</span>
</div>
</template>
<script>
export default {
name: 'GenericPill',
props: {
item_list: {required: true, type: Array},
label: {type: String, default: 'name'},
color: {type: String, default: 'light'}
},
computed: {
itemList: function() {
if(Array.isArray(this.item_list)) {
return this.item_list
} else if (!this.item_list?.id) {
return false
} else {
return [this.item_list]
}
name: "GenericPill",
props: {
item_list: {
type: Array,
default() {
return []
},
},
label: { type: String, default: "name" },
color: { type: String, default: "light" },
},
computed: {
itemList: function () {
if (Array.isArray(this.item_list)) {
return this.item_list
} else if (!this.item_list?.id) {
return false
} else {
return [this.item_list]
}
},
},
mounted() {},
methods: {
thisLabel: function (item) {
let fields = this.label.split("::")
let value = item
fields.forEach((x) => {
value = value[x]
})
return value
},
},
},
mounted() {
},
methods: {
thisLabel: function (item) {
let fields = this.label.split('::')
let value = item
fields.forEach(x => {
value = value[x]
});
return value
}
}
}
</script>

View File

@@ -1,143 +1,250 @@
<template>
<div>
<b-modal :id="'modal_'+id" @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>
<!-- this lookup is single selection -->
<lookup-input v-if="f.type=='lookup'"
:form="f"
:model="listModel(f.list)"
@change="storeValue"/> <!-- TODO add ability to create new items associated with lookup -->
<!-- TODO: add multi-selection input list -->
<checkbox-input v-if="f.type=='checkbox'"
:label="f.label"
:value="f.value"
:field="f.field"/>
<text-input v-if="f.type=='text'"
:label="f.label"
:value="f.value"
:field="f.field"
:placeholder="f.placeholder"/>
<choice-input v-if="f.type=='choice'"
:label="f.label"
:value="f.value"
:field="f.field"
:options="f.options"
:placeholder="f.placeholder"/>
<emoji-input v-if="f.type=='emoji'"
:label="f.label"
:value="f.value"
:field="f.field"
@change="storeValue"/>
<file-input v-if="f.type=='file'"
:label="f.label"
:value="f.value"
:field="f.field"
@change="storeValue"/>
</div>
<div>
<b-modal :id="'modal_' + id" @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>
<!-- this lookup is single selection -->
<lookup-input v-if="f.type == 'lookup'" :form="f" :model="listModel(f.list)" @change="storeValue" />
<!-- TODO add ability to create new items associated with lookup -->
<!-- TODO: add multi-selection input list -->
<checkbox-input v-if="f.type == 'checkbox'" :label="f.label" :value="f.value" :field="f.field" />
<text-input v-if="f.type == 'text'" :label="f.label" :value="f.value" :field="f.field" :placeholder="f.placeholder" />
<choice-input v-if="f.type == 'choice'" :label="f.label" :value="f.value" :field="f.field" :options="f.options" :placeholder="f.placeholder" />
<emoji-input v-if="f.type == 'emoji'" :label="f.label" :value="f.value" :field="f.field" @change="storeValue" />
<file-input v-if="f.type == 'file'" :label="f.label" :value="f.value" :field="f.field" @change="storeValue" />
</div>
<template v-slot:modal-footer>
<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>
</div>
<template v-slot:modal-footer>
<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>
</div>
</template>
<script>
import Vue from 'vue'
import {BootstrapVue} from 'bootstrap-vue'
import {getForm} from "@/utils/utils";
import Vue from "vue"
import { BootstrapVue } from "bootstrap-vue"
import { getForm } from "@/utils/utils"
Vue.use(BootstrapVue)
import {Models} from "@/utils/models";
import CheckboxInput from "@/components/Modals/CheckboxInput";
import LookupInput from "@/components/Modals/LookupInput";
import TextInput from "@/components/Modals/TextInput";
import EmojiInput from "@/components/Modals/EmojiInput";
import ChoiceInput from "@/components/Modals/ChoiceInput";
import FileInput from "@/components/Modals/FileInput";
import { ApiApiFactory } from "@/utils/openapi/api"
import { ApiMixin, StandardToasts, ToastMixin } from "@/utils/utils"
import CheckboxInput from "@/components/Modals/CheckboxInput"
import LookupInput from "@/components/Modals/LookupInput"
import TextInput from "@/components/Modals/TextInput"
import EmojiInput from "@/components/Modals/EmojiInput"
import ChoiceInput from "@/components/Modals/ChoiceInput"
import FileInput from "@/components/Modals/FileInput"
export default {
name: 'GenericModalForm',
components: {FileInput, CheckboxInput, LookupInput, TextInput, EmojiInput, ChoiceInput},
props: {
model: {required: true, type: Object},
action: {required: true, type: Object},
item1: {
type: Object, default() {
return undefined
}
name: "GenericModalForm",
components: { FileInput, CheckboxInput, LookupInput, TextInput, EmojiInput, ChoiceInput },
mixins: [ApiMixin, ToastMixin],
props: {
model: { required: true, type: Object },
action: { type: Object },
item1: {
type: Object,
default() {
return {}
},
},
item2: {
type: Object,
default() {
return {}
},
},
show: { required: true, type: Boolean, default: false },
},
item2: {
type: Object, default() {
return undefined
}
},
show: {required: true, type: Boolean, default: false},
},
data() {
return {
id: undefined,
form_data: {},
form: {},
dirty: false,
special_handling: false
}
},
mounted() {
this.id = Math.random()
this.$root.$on('change', this.storeValue); // boostrap modal placed at document so have to listen at root of component
},
computed: {
buttonLabel() {
return this.buttons[this.action].label;
},
},
watch: {
'show': function () {
if (this.show) {
this.form = getForm(this.model, this.action, this.item1, this.item2)
this.dirty = true
this.$bvModal.show('modal_' + this.id)
} else {
this.$bvModal.hide('modal_' + this.id)
this.form_data = {}
}
},
},
methods: {
doAction: function () {
this.dirty = false
this.$emit('finish-action', {'form_data': this.detectOverride(this.form_data)})
},
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') {
return this.model
} else {
return Models[m]
}
},
detectOverride: function (form) {
for (const [k, v] of Object.entries(form)) {
if (form[k].__override__) {
form[k] = form[k].__override__
data() {
return {
id: undefined,
form_data: {},
form: {},
dirty: false,
special_handling: false,
}
}
return form
}
}
},
mounted() {
this.id = Math.random()
this.$root.$on("change", this.storeValue) // boostrap modal placed at document so have to listen at root of component
},
computed: {
buttonLabel() {
return this.buttons[this.action].label
},
},
watch: {
show: function () {
if (this.show) {
this.form = getForm(this.model, this.action, this.item1, this.item2)
this.dirty = true
this.$bvModal.show("modal_" + this.id)
} else {
this.$bvModal.hide("modal_" + this.id)
this.form_data = {}
}
},
},
methods: {
doAction: function () {
this.dirty = false
switch (this.action) {
case this.Actions.DELETE:
this.delete()
break
case this.Actions.CREATE:
this.save()
break
case this.Actions.UPDATE:
this.form_data.id = this.item1.id
this.save()
break
case this.Actions.MERGE:
this.merge(this.item1, this.form_data.target.id, this.item1?.automate ?? false)
break
case this.Actions.MOVE:
this.move(this.item1.id, this.form_data.target.id)
break
}
},
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") {
return this.model
} else {
return this.Models[m]
}
},
detectOverride: function (form) {
for (const [k, v] of Object.entries(form)) {
if (form[k].__override__) {
form[k] = form[k].__override__
}
}
return form
},
delete: function () {
this.genericAPI(this.model, this.Actions.DELETE, { id: this.item1.id })
.then((result) => {
this.$emit("finish-action")
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_DELETE)
})
.catch((err) => {
console.log(err)
StandardToasts.makeStandardToast(StandardToasts.FAIL_DELETE)
this.$emit("finish-action", "cancel")
})
},
save: function () {
if (!this.item1?.id) {
// if there is no item id assume it's a new item
this.genericAPI(this.model, this.Actions.CREATE, this.form_data)
.then((result) => {
this.$emit("finish-action", { item: result.data })
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
})
.catch((err) => {
console.log(err)
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
this.$emit("finish-action", "cancel")
})
} else {
this.genericAPI(this.model, this.Actions.UPDATE, this.form_data)
.then((result) => {
this.$emit("finish-action")
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE)
})
.catch((err) => {
console.log(err, err.response)
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
this.$emit("finish-action", "cancel")
})
}
},
move: function () {
if (this.item1.id === this.form_data.target.id) {
this.makeToast(this.$t("Error"), this.$t("err_move_self"), "danger")
this.$emit("finish-action", "cancel")
return
}
if (this.form_data.target.id === undefined || this.item1?.parent == this.form_data.target.id) {
this.makeToast(this.$t("Warning"), this.$t("nothing"), "warning")
this.$emit("finish-action", "cancel")
return
}
this.genericAPI(this.model, this.Actions.MOVE, { source: this.item1.id, target: this.form_data.target.id })
.then((result) => {
this.$emit("finish-action", { target: this.form_data.target.id })
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_MOVE)
})
.catch((err) => {
console.log(err)
StandardToasts.makeStandardToast(StandardToasts.FAIL_MOVE)
this.$emit("finish-action", "cancel")
})
},
merge: function () {
if (this.item1.id === this.form_data.target.id) {
this.makeToast(this.$t("Error"), this.$t("err_merge_self"), "danger")
this.$emit("finish-action", "cancel")
return
}
if (!this.item1.id || !this.form_data.target.id) {
this.makeToast(this.$t("Warning"), this.$t("nothing"), "warning")
this.$emit("finish-action", "cancel")
return
}
this.genericAPI(this.model, this.Actions.MERGE, {
source: this.item1.id,
target: this.form_data.target.id,
})
.then((result) => {
this.$emit("finish-action", { target: this.form_data.target.id })
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_MERGE)
})
.catch((err) => {
//TODO error checking not working with OpenAPI methods
console.log("Error", err)
StandardToasts.makeStandardToast(StandardToasts.FAIL_MERGE)
this.$emit("finish-action", "cancel")
})
if (this.item1.automate) {
let apiClient = new ApiApiFactory()
let automation = {
name: `Merge ${this.item1.name} with ${this.form_data.target.name}`,
param_1: this.item1.name,
param_2: this.form_data.target.name,
}
if (this.model === this.Models.FOOD) {
automation.type = "FOOD_ALIAS"
}
if (this.model === this.Models.UNIT) {
automation.type = "UNIT_ALIAS"
}
if (this.model === this.Models.KEYWORD) {
automation.type = "KEYWORD_ALIAS"
}
apiClient.createAutomation(automation)
}
},
},
}
</script>
</script>

View File

@@ -1,157 +1,171 @@
<template>
<div>
<b-form-group
v-bind:label="form.label"
class="mb-3">
<generic-multiselect
@change="new_value=$event.val"
@remove="new_value=undefined"
:initial_selection="initialSelection"
:model="model"
:multiple="useMultiple"
:sticky_options="sticky_options"
:allow_create="create_new"
:create_placeholder="createPlaceholder"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
:placeholder="modelName"
@new="addNew">
</generic-multiselect>
<b-form-group class="mb-3">
<template #label v-if="show_label">
{{ form.label }}
</template>
<generic-multiselect
@change="new_value = $event.val"
@remove="new_value = undefined"
:initial_selection="initialSelection"
:model="model"
:multiple="useMultiple"
:sticky_options="sticky_options"
:allow_create="form.allow_create"
:create_placeholder="createPlaceholder"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
:placeholder="modelName"
@new="addNew"
>
</generic-multiselect>
</b-form-group>
</div>
</template>
<script>
import GenericMultiselect from "@/components/GenericMultiselect";
import {StandardToasts, ApiMixin} from "@/utils/utils";
import GenericMultiselect from "@/components/GenericMultiselect"
import { StandardToasts, ApiMixin } from "@/utils/utils"
export default {
name: 'LookupInput',
components: {GenericMultiselect},
mixins: [ApiMixin],
props: {
form: {type: Object, default () {return undefined}},
model: {type: Object, default () {return undefined}},
// TODO: include create_new and create_text props and associated functionality to create objects for drop down
// see 'tagging' here: https://vue-multiselect.js.org/#sub-tagging
// perfect world would have it trigger a new modal associated with the associated item model
},
data() {
return {
new_value: undefined,
field: undefined,
label: undefined,
sticky_options: undefined,
first_run: true
}
},
mounted() {
this.new_value = this.form?.value
this.field = this.form?.field ?? 'You Forgot To Set Field Name'
this.label = this.form?.label ?? ''
this.sticky_options = this.form?.sticky_options ?? []
},
computed: {
modelName() {
return this?.model?.name ?? this.$t('Search')
name: "LookupInput",
components: { GenericMultiselect },
mixins: [ApiMixin],
props: {
form: {
type: Object,
default() {
return undefined
},
},
model: {
type: Object,
default() {
return undefined
},
},
show_label: { type: Boolean, default: true },
},
useMultiple() {
return this.form?.multiple || this.form?.ordered || false
},
initialSelection() {
let this_value = this.form.value
let arrayValues = undefined
// multiselect is expect to get an array of objects - make sure it gets one
if (Array.isArray(this_value)) {
arrayValues = this_value
} else if (!this_value) {
arrayValues = []
} else if (typeof(this_value) === 'object') {
arrayValues = [this_value]
} else {
arrayValues = [{'id': -1, 'name': this_value}]
}
if (this.form?.ordered && this.first_run) {
return this.flattenItems(arrayValues)
} else {
return arrayValues
}
},
createPlaceholder() {
return this.$t('Create_New_' + this?.model?.name)
}
},
watch: {
'new_value': function () {
let x = this?.new_value
// pass the unflattened attributes that can be restored when ready to save/update
if (this.form?.ordered) {
x['__override__'] = this.unflattenItem(this?.new_value)
}
this.$root.$emit('change', this.form.field, x)
},
},
methods: {
addNew: function(e) {
// if create a new item requires more than 1 parameter or the field 'name' is insufficient this will need reworked
// in a perfect world this would trigger a new modal and allow editing all fields
this.genericAPI(this.model, this.Actions.CREATE, {'name': e}).then((result) => {
this.new_value = result.data
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
}).catch((err) => {
console.log(err)
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
})
},
// ordered lookups have nested attributes that need flattened attributes to drive lookup
flattenItems: function(itemlist) {
let flat_items = []
let item = undefined
let label = this.form.list_label.split('::')
itemlist.forEach(x => {
item = {}
for (const [k, v] of Object.entries(x)) {
if (k == label[0]) {
item['id'] = v.id
item[label[1]] = v[label[1]]
} else {
item[this.form.field + '__' + k] = v
}
data() {
return {
new_value: undefined,
field: undefined,
label: undefined,
sticky_options: undefined,
first_run: true,
}
flat_items.push(item)
});
this.first_run = false
return flat_items
},
unflattenItem: function(itemList) {
let unflat_items = []
let item = undefined
let this_label = undefined
let label = this.form.list_label.split('::')
let order = 0
itemList.forEach(x => {
item = {}
item[label[0]] = {}
for (const [k, v] of Object.entries(x)) {
switch(k) {
case 'id':
item[label[0]]['id'] = v
break;
case label[1]:
item[label[0]][label[1]] = v
break;
default:
this_label = k.replace(this.form.field + '__', '')
}
}
item['order'] = order
order++
unflat_items.push(item)
});
return unflat_items
}
}
mounted() {
this.new_value = this.form?.value
this.field = this.form?.field ?? "You Forgot To Set Field Name"
this.label = this.form?.label ?? ""
this.sticky_options = this.form?.sticky_options ?? []
},
computed: {
modelName() {
return this?.model?.name ?? this.$t("Search")
},
useMultiple() {
return this.form?.multiple || this.form?.ordered || false
},
initialSelection() {
let this_value = this.new_value
let arrayValues = undefined
// multiselect is expect to get an array of objects - make sure it gets one
if (Array.isArray(this_value)) {
arrayValues = this_value
} else if (!this_value) {
arrayValues = []
} else if (typeof this_value === "object") {
arrayValues = [this_value]
} else {
arrayValues = [{ id: -1, name: this_value }]
}
if (this.form?.ordered && this.first_run) {
return this.flattenItems(arrayValues)
} else {
return arrayValues
}
},
createPlaceholder() {
return this.$t("Create_New_" + this?.model?.name)
},
},
watch: {
"form.value": function (newVal, oldVal) {
this.new_value = newVal
},
new_value: function () {
let x = this?.new_value
// pass the unflattened attributes that can be restored when ready to save/update
if (this.form?.ordered) {
x["__override__"] = this.unflattenItem(this?.new_value)
}
this.$root.$emit("change", this.form.field, x)
this.$emit("change", x)
},
},
methods: {
addNew: function (e) {
// if create a new item requires more than 1 parameter or the field 'name' is insufficient this will need reworked
// in a perfect world this would trigger a new modal and allow editing all fields
this.genericAPI(this.model, this.Actions.CREATE, { name: e })
.then((result) => {
this.new_value = result.data
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
})
.catch((err) => {
console.log(err)
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
})
},
// ordered lookups have nested attributes that need flattened attributes to drive lookup
flattenItems: function (itemlist) {
let flat_items = []
let item = undefined
let label = this.form.list_label.split("::")
itemlist.forEach((x) => {
item = {}
for (const [k, v] of Object.entries(x)) {
if (k == label[0]) {
item["id"] = v.id
item[label[1]] = v[label[1]]
} else {
item[this.form.field + "__" + k] = v
}
}
flat_items.push(item)
})
this.first_run = false
return flat_items
},
unflattenItem: function (itemList) {
let unflat_items = []
let item = undefined
let this_label = undefined
let label = this.form.list_label.split("::")
let order = 0
itemList.forEach((x) => {
item = {}
item[label[0]] = {}
for (const [k, v] of Object.entries(x)) {
switch (k) {
case "id":
item[label[0]]["id"] = v
break
case label[1]:
item[label[0]][label[1]] = v
break
default:
this_label = k.replace(this.form.field + "__", "")
}
}
item["order"] = order
order++
unflat_items.push(item)
})
return unflat_items
},
},
}
</script>
</script>

View File

@@ -1,210 +1,217 @@
{
"warning_feature_beta": "This feature is currently in a BETA (testing) state. Please expect bugs and possibly breaking changes in the future (possibly loosing feature related data) when using this feature.",
"err_fetching_resource": "There was an error fetching a resource!",
"err_creating_resource": "There was an error creating a resource!",
"err_updating_resource": "There was an error updating a resource!",
"err_deleting_resource": "There was an error deleting a resource!",
"success_fetching_resource": "Successfully fetched a resource!",
"success_creating_resource": "Successfully created a resource!",
"success_updating_resource": "Successfully updated a resource!",
"success_deleting_resource": "Successfully deleted a resource!",
"file_upload_disabled": "File upload is not enabled for your space.",
"step_time_minutes": "Step time in minutes",
"confirm_delete": "Are you sure you want to delete this {object}?",
"import_running": "Import running, please wait!",
"all_fields_optional": "All fields are optional and can be left empty.",
"convert_internal": "Convert to internal recipe",
"show_only_internal": "Show only internal recipes",
"show_split_screen": "Split View",
"Log_Recipe_Cooking": "Log Recipe Cooking",
"External_Recipe_Image": "External Recipe Image",
"Add_to_Shopping": "Add to Shopping",
"Add_to_Plan": "Add to Plan",
"Step_start_time": "Step start time",
"Sort_by_new": "Sort by new",
"Table_of_Contents": "Table of Contents",
"Recipes_per_page": "Recipes per Page",
"Show_as_header": "Show as header",
"Hide_as_header": "Hide as header",
"Add_nutrition_recipe": "Add nutrition to recipe",
"Remove_nutrition_recipe": "Delete nutrition from recipe",
"Copy_template_reference": "Copy template reference",
"Save_and_View": "Save & View",
"Manage_Books": "Manage Books",
"Meal_Plan": "Meal Plan",
"Select_Book": "Select Book",
"Select_File": "Select File",
"Recipe_Image": "Recipe Image",
"Import_finished": "Import finished",
"View_Recipes": "View Recipes",
"Log_Cooking": "Log Cooking",
"New_Recipe": "New Recipe",
"Url_Import": "Url Import",
"Reset_Search": "Reset Search",
"Recently_Viewed": "Recently Viewed",
"Load_More": "Load More",
"New_Keyword": "New Keyword",
"Delete_Keyword": "Delete Keyword",
"Edit_Keyword": "Edit Keyword",
"Edit_Recipe": "Edit Recipe",
"Move_Keyword": "Move Keyword",
"Merge_Keyword": "Merge Keyword",
"Hide_Keywords": "Hide Keyword",
"Hide_Recipes": "Hide Recipes",
"Move_Up": "Move up",
"Move_Down": "Move down",
"Step_Name": "Step Name",
"Step_Type": "Step Type",
"Make_header": "Make_Header",
"Make_Ingredient": "Make_Ingredient",
"Enable_Amount": "Enable Amount",
"Disable_Amount": "Disable Amount",
"Add_Step": "Add Step",
"Keywords": "Keywords",
"Books": "Books",
"Proteins": "Proteins",
"Fats": "Fats",
"Carbohydrates": "Carbohydrates",
"Calories": "Calories",
"Energy": "Energy",
"Nutrition": "Nutrition",
"Date": "Date",
"Share": "Share",
"Automation": "Automation",
"Parameter": "Parameter",
"Export": "Export",
"Copy": "Copy",
"Rating": "Rating",
"Close": "Close",
"Cancel": "Cancel",
"Link": "Link",
"Add": "Add",
"New": "New",
"Note": "Note",
"Success": "Success",
"Failure": "Failure",
"Ingredients": "Ingredients",
"Supermarket": "Supermarket",
"Categories": "Categories",
"Category": "Category",
"Selected": "Selected",
"min": "min",
"Servings": "Servings",
"Waiting": "Waiting",
"Preparation": "Preparation",
"External": "External",
"Size": "Size",
"Files": "Files",
"File": "File",
"Edit": "Edit",
"Image": "Image",
"Delete": "Delete",
"Open": "Open",
"Ok": "Open",
"Save": "Save",
"Step": "Step",
"Search": "Search",
"Import": "Import",
"Print": "Print",
"Settings": "Settings",
"or": "or",
"and": "and",
"Information": "Information",
"Download": "Download",
"Create": "Create",
"Advanced Search Settings": "Advanced Search Settings",
"View": "View",
"Recipes": "Recipes",
"Move": "Move",
"Merge": "Merge",
"Parent": "Parent",
"delete_confirmation": "Are you sure that you want to delete {source}?",
"move_confirmation": "Move <i>{child}</i> to parent <i>{parent}</i>",
"merge_confirmation": "Replace <i>{source}</i> with <i>{target}</i>",
"create_rule": "and create automation",
"move_selection": "Select a parent {type} to move {source} to.",
"merge_selection": "Replace all occurrences of {source} with the selected {type}.",
"Root": "Root",
"Ignore_Shopping": "Ignore Shopping",
"Shopping_Category": "Shopping Category",
"Edit_Food": "Edit Food",
"Move_Food": "Move Food",
"New_Food": "New Food",
"Hide_Food": "Hide Food",
"Food_Alias": "Food Alias",
"Unit_Alias": "Unit Alias",
"Keyword_Alias": "Keyword Alias",
"Delete_Food": "Delete Food",
"No_ID": "ID not found, cannot delete.",
"Meal_Plan_Days": "Future meal plans",
"merge_title": "Merge {type}",
"move_title": "Move {type}",
"Food": "Food",
"Recipe_Book": "Recipe Book",
"del_confirmation_tree": "Are you sure that you want to delete {source} and all of it's children?",
"delete_title": "Delete {type}",
"create_title": "New {type}",
"edit_title": "Edit {type}",
"Name": "Name",
"Type": "Type",
"Description": "Description",
"Recipe": "Recipe",
"tree_root": "Root of Tree",
"Icon": "Icon",
"Unit": "Unit",
"No_Results": "No Results",
"New_Unit": "New Unit",
"Create_New_Shopping Category": "Create New Shopping Category",
"Create_New_Food": "Add New Food",
"Create_New_Keyword": "Add New Keyword",
"Create_New_Unit": "Add New Unit",
"Create_New_Meal_Type": "Add New Meal Type",
"and_up": "& Up",
"Instructions": "Instructions",
"Unrated": "Unrated",
"Automate": "Automate",
"Empty": "Empty",
"Key_Ctrl": "Ctrl",
"Key_Shift": "Shift",
"Time": "Time",
"Text": "Text",
"Shopping_list": "Shopping List",
"Create_Meal_Plan_Entry": "Create meal plan entry",
"Edit_Meal_Plan_Entry": "Edit meal plan entry",
"Title": "Title",
"Week": "Week",
"Month": "Month",
"Year": "Year",
"Planner": "Planner",
"Planner_Settings": "Planner settings",
"Period": "Period",
"Plan_Period_To_Show": "Show weeks, months or years",
"Periods": "Periods",
"Plan_Show_How_Many_Periods": "How many periods to show",
"Starting_Day": "Starting day of the week",
"Meal_Types": "Meal types",
"Meal_Type": "Meal type",
"Clone": "Clone",
"Drag_Here_To_Delete": "Drag here to delete",
"Meal_Type_Required": "Meal type is required",
"Title_or_Recipe_Required": "Title or recipe selection required",
"Color": "Color",
"New_Meal_Type": "New Meal type",
"Week_Numbers": "Week numbers",
"Show_Week_Numbers": "Show week numbers ?",
"Export_As_ICal": "Export current period to iCal format",
"Export_To_ICal": "Export .ics",
"Cannot_Add_Notes_To_Shopping": "Notes cannot be added to the shopping list",
"Added_To_Shopping_List": "Added to shopping list",
"Shopping_List_Empty": "Your shopping list is currently empty, you can add items via the context menu of a meal plan entry (right click on the card or left click the menu icon)",
"Next_Period": "Next Period",
"Previous_Period": "Previous Period",
"Current_Period": "Current Period",
"Next_Day": "Next Day",
"Previous_Day": "Previous Day",
"Coming_Soon": "Coming-Soon",
"Auto_Planner": "Auto-Planner",
"New_Cookbook": "New cookbook",
"Hide_Keyword": "Hide keywords",
"Clear": "Clear"
"warning_feature_beta": "This feature is currently in a BETA (testing) state. Please expect bugs and possibly breaking changes in the future (possibly loosing feature related data) when using this feature.",
"err_fetching_resource": "There was an error fetching a resource!",
"err_creating_resource": "There was an error creating a resource!",
"err_updating_resource": "There was an error updating a resource!",
"err_deleting_resource": "There was an error deleting a resource!",
"err_moving_resource": "There was an error moving a resource!",
"err_merging_resource": "There was an error merging a resource!",
"success_fetching_resource": "Successfully fetched a resource!",
"success_creating_resource": "Successfully created a resource!",
"success_updating_resource": "Successfully updated a resource!",
"success_deleting_resource": "Successfully deleted a resource!",
"success_moving_resource": "Successfully moved a resource!",
"success_merging_resource": "Successfully merged a resource!",
"file_upload_disabled": "File upload is not enabled for your space.",
"step_time_minutes": "Step time in minutes",
"confirm_delete": "Are you sure you want to delete this {object}?",
"import_running": "Import running, please wait!",
"all_fields_optional": "All fields are optional and can be left empty.",
"convert_internal": "Convert to internal recipe",
"show_only_internal": "Show only internal recipes",
"show_split_screen": "Split View",
"Log_Recipe_Cooking": "Log Recipe Cooking",
"External_Recipe_Image": "External Recipe Image",
"Add_to_Shopping": "Add to Shopping",
"Add_to_Plan": "Add to Plan",
"Step_start_time": "Step start time",
"Sort_by_new": "Sort by new",
"Table_of_Contents": "Table of Contents",
"Recipes_per_page": "Recipes per Page",
"Show_as_header": "Show as header",
"Hide_as_header": "Hide as header",
"Add_nutrition_recipe": "Add nutrition to recipe",
"Remove_nutrition_recipe": "Delete nutrition from recipe",
"Copy_template_reference": "Copy template reference",
"Save_and_View": "Save & View",
"Manage_Books": "Manage Books",
"Meal_Plan": "Meal Plan",
"Select_Book": "Select Book",
"Select_File": "Select File",
"Recipe_Image": "Recipe Image",
"Import_finished": "Import finished",
"View_Recipes": "View Recipes",
"Log_Cooking": "Log Cooking",
"New_Recipe": "New Recipe",
"Url_Import": "Url Import",
"Reset_Search": "Reset Search",
"Recently_Viewed": "Recently Viewed",
"Load_More": "Load More",
"New_Keyword": "New Keyword",
"Delete_Keyword": "Delete Keyword",
"Edit_Keyword": "Edit Keyword",
"Edit_Recipe": "Edit Recipe",
"Move_Keyword": "Move Keyword",
"Merge_Keyword": "Merge Keyword",
"Hide_Keywords": "Hide Keyword",
"Hide_Recipes": "Hide Recipes",
"Move_Up": "Move up",
"Move_Down": "Move down",
"Step_Name": "Step Name",
"Step_Type": "Step Type",
"Make_header": "Make_Header",
"Make_Ingredient": "Make_Ingredient",
"Enable_Amount": "Enable Amount",
"Disable_Amount": "Disable Amount",
"Add_Step": "Add Step",
"Keywords": "Keywords",
"Books": "Books",
"Proteins": "Proteins",
"Fats": "Fats",
"Carbohydrates": "Carbohydrates",
"Calories": "Calories",
"Energy": "Energy",
"Nutrition": "Nutrition",
"Date": "Date",
"Share": "Share",
"Automation": "Automation",
"Parameter": "Parameter",
"Export": "Export",
"Copy": "Copy",
"Rating": "Rating",
"Close": "Close",
"Cancel": "Cancel",
"Link": "Link",
"Add": "Add",
"New": "New",
"Note": "Note",
"Success": "Success",
"Failure": "Failure",
"Ingredients": "Ingredients",
"Supermarket": "Supermarket",
"Categories": "Categories",
"Category": "Category",
"Selected": "Selected",
"min": "min",
"Servings": "Servings",
"Waiting": "Waiting",
"Preparation": "Preparation",
"External": "External",
"Size": "Size",
"Files": "Files",
"File": "File",
"Edit": "Edit",
"Image": "Image",
"Delete": "Delete",
"Open": "Open",
"Ok": "Open",
"Save": "Save",
"Step": "Step",
"Search": "Search",
"Import": "Import",
"Print": "Print",
"Settings": "Settings",
"or": "or",
"and": "and",
"Information": "Information",
"Download": "Download",
"Create": "Create",
"Advanced Search Settings": "Advanced Search Settings",
"View": "View",
"Recipes": "Recipes",
"Move": "Move",
"Merge": "Merge",
"Parent": "Parent",
"delete_confirmation": "Are you sure that you want to delete {source}?",
"move_confirmation": "Move <i>{child}</i> to parent <i>{parent}</i>",
"merge_confirmation": "Replace <i>{source}</i> with <i>{target}</i>",
"create_rule": "and create automation",
"move_selection": "Select a parent {type} to move {source} to.",
"merge_selection": "Replace all occurrences of {source} with the selected {type}.",
"Root": "Root",
"Ignore_Shopping": "Ignore Shopping",
"Shopping_Category": "Shopping Category",
"Edit_Food": "Edit Food",
"Move_Food": "Move Food",
"New_Food": "New Food",
"Hide_Food": "Hide Food",
"Food_Alias": "Food Alias",
"Unit_Alias": "Unit Alias",
"Keyword_Alias": "Keyword Alias",
"Delete_Food": "Delete Food",
"No_ID": "ID not found, cannot delete.",
"Meal_Plan_Days": "Future meal plans",
"merge_title": "Merge {type}",
"move_title": "Move {type}",
"Food": "Food",
"Recipe_Book": "Recipe Book",
"del_confirmation_tree": "Are you sure that you want to delete {source} and all of it's children?",
"delete_title": "Delete {type}",
"create_title": "New {type}",
"edit_title": "Edit {type}",
"Name": "Name",
"Type": "Type",
"Description": "Description",
"Recipe": "Recipe",
"tree_root": "Root of Tree",
"Icon": "Icon",
"Unit": "Unit",
"No_Results": "No Results",
"New_Unit": "New Unit",
"Create_New_Shopping Category": "Create New Shopping Category",
"Create_New_Food": "Add New Food",
"Create_New_Keyword": "Add New Keyword",
"Create_New_Unit": "Add New Unit",
"Create_New_Meal_Type": "Add New Meal Type",
"and_up": "& Up",
"Instructions": "Instructions",
"Unrated": "Unrated",
"Automate": "Automate",
"Empty": "Empty",
"Key_Ctrl": "Ctrl",
"Key_Shift": "Shift",
"Time": "Time",
"Text": "Text",
"Shopping_list": "Shopping List",
"Create_Meal_Plan_Entry": "Create meal plan entry",
"Edit_Meal_Plan_Entry": "Edit meal plan entry",
"Title": "Title",
"Week": "Week",
"Month": "Month",
"Year": "Year",
"Planner": "Planner",
"Planner_Settings": "Planner settings",
"Period": "Period",
"Plan_Period_To_Show": "Show weeks, months or years",
"Periods": "Periods",
"Plan_Show_How_Many_Periods": "How many periods to show",
"Starting_Day": "Starting day of the week",
"Meal_Types": "Meal types",
"Meal_Type": "Meal type",
"Clone": "Clone",
"Drag_Here_To_Delete": "Drag here to delete",
"Meal_Type_Required": "Meal type is required",
"Title_or_Recipe_Required": "Title or recipe selection required",
"Color": "Color",
"New_Meal_Type": "New Meal type",
"Week_Numbers": "Week numbers",
"Show_Week_Numbers": "Show week numbers ?",
"Export_As_ICal": "Export current period to iCal format",
"Export_To_ICal": "Export .ics",
"Cannot_Add_Notes_To_Shopping": "Notes cannot be added to the shopping list",
"Added_To_Shopping_List": "Added to shopping list",
"Shopping_List_Empty": "Your shopping list is currently empty, you can add items via the context menu of a meal plan entry (right click on the card or left click the menu icon)",
"Next_Period": "Next Period",
"Previous_Period": "Previous Period",
"Current_Period": "Current Period",
"Next_Day": "Next Day",
"Previous_Day": "Previous Day",
"Coming_Soon": "Coming-Soon",
"Auto_Planner": "Auto-Planner",
"New_Cookbook": "New cookbook",
"Hide_Keyword": "Hide keywords",
"Clear": "Clear",
"err_move_self": "Cannot move item to itself",
"nothing": "Nothing to do",
"err_merge_self": "Cannot merge item with itself"
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,26 @@
/*
* Utility functions to call bootstrap toasts
* */
import {BToast} from 'bootstrap-vue'
import i18n from "@/i18n";
* Utility functions to call bootstrap toasts
* */
import i18n from "@/i18n"
import { frac } from "@/utils/fractions"
/*
* Utility functions to use OpenAPIs generically
* */
import { ApiApiFactory } from "@/utils/openapi/api.ts"
import axios from "axios"
import { BToast } from "bootstrap-vue"
// /*
// * Utility functions to use manipulate nested components
// * */
import Vue from "vue"
import { Actions, Models } from "./models"
export const ToastMixin = {
methods: {
makeToast: function (title, message, variant = null) {
return makeToast(title, message, variant)
}
}
},
},
}
export function makeToast(title, message, variant = null) {
@@ -17,57 +28,71 @@ export function makeToast(title, message, variant = null) {
toaster.$bvToast.toast(message, {
title: title,
variant: variant,
toaster: 'b-toaster-bottom-right',
solid: true
toaster: "b-toaster-bottom-right",
solid: true,
})
}
export class StandardToasts {
static SUCCESS_CREATE = 'SUCCESS_CREATE'
static SUCCESS_FETCH = 'SUCCESS_FETCH'
static SUCCESS_UPDATE = 'SUCCESS_UPDATE'
static SUCCESS_DELETE = 'SUCCESS_DELETE'
static SUCCESS_CREATE = "SUCCESS_CREATE"
static SUCCESS_FETCH = "SUCCESS_FETCH"
static SUCCESS_UPDATE = "SUCCESS_UPDATE"
static SUCCESS_DELETE = "SUCCESS_DELETE"
static SUCCESS_MOVE = "SUCCESS_MOVE"
static SUCCESS_MERGE = "SUCCESS_MERGE"
static FAIL_CREATE = 'FAIL_CREATE'
static FAIL_FETCH = 'FAIL_FETCH'
static FAIL_UPDATE = 'FAIL_UPDATE'
static FAIL_DELETE = 'FAIL_DELETE'
static FAIL_CREATE = "FAIL_CREATE"
static FAIL_FETCH = "FAIL_FETCH"
static FAIL_UPDATE = "FAIL_UPDATE"
static FAIL_DELETE = "FAIL_DELETE"
static FAIL_MOVE = "FAIL_MOVE"
static FAIL_MERGE = "FAIL_MERGE"
static makeStandardToast(toast) {
static makeStandardToast(toast, err_details = undefined) {
switch (toast) {
case StandardToasts.SUCCESS_CREATE:
makeToast(i18n.tc('Success'), i18n.tc('success_creating_resource'), 'success')
break;
makeToast(i18n.tc("Success"), i18n.tc("success_creating_resource"), "success")
break
case StandardToasts.SUCCESS_FETCH:
makeToast(i18n.tc('Success'), i18n.tc('success_fetching_resource'), 'success')
break;
makeToast(i18n.tc("Success"), i18n.tc("success_fetching_resource"), "success")
break
case StandardToasts.SUCCESS_UPDATE:
makeToast(i18n.tc('Success'), i18n.tc('success_updating_resource'), 'success')
break;
makeToast(i18n.tc("Success"), i18n.tc("success_updating_resource"), "success")
break
case StandardToasts.SUCCESS_DELETE:
makeToast(i18n.tc('Success'), i18n.tc('success_deleting_resource'), 'success')
break;
makeToast(i18n.tc("Success"), i18n.tc("success_deleting_resource"), "success")
break
case StandardToasts.SUCCESS_MOVE:
makeToast(i18n.tc("Success"), i18n.tc("success_moving_resource"), "success")
break
case StandardToasts.SUCCESS_MERGE:
makeToast(i18n.tc("Success"), i18n.tc("success_merging_resource"), "success")
break
case StandardToasts.FAIL_CREATE:
makeToast(i18n.tc('Failure'), i18n.tc('err_creating_resource'), 'danger')
break;
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')
break;
makeToast(i18n.tc("Failure"), i18n.tc("err_fetching_resource"), "danger")
break
case StandardToasts.FAIL_UPDATE:
makeToast(i18n.tc('Failure'), i18n.tc('err_updating_resource'), 'danger')
break;
makeToast(i18n.tc("Failure"), i18n.tc("err_updating_resource"), "danger")
break
case StandardToasts.FAIL_DELETE:
makeToast(i18n.tc('Failure'), i18n.tc('err_deleting_resource'), 'danger')
break;
makeToast(i18n.tc("Failure"), i18n.tc("err_deleting_resource"), "danger")
break
case StandardToasts.FAIL_MOVE:
makeToast(i18n.tc("Failure"), i18n.tc("err_moving_resource") + (err_details ? "\n" + err_details : ""), "danger")
break
case StandardToasts.FAIL_MERGE:
makeToast(i18n.tc("Failure"), i18n.tc("err_merging_resource") + (err_details ? "\n" + err_details : ""), "danger")
break
}
}
}
/*
* Utility functions to use djangos gettext
* */
* Utility functions to use djangos gettext
* */
export const GettextMixin = {
methods: {
@@ -77,8 +102,8 @@ export const GettextMixin = {
*/
_: function (param) {
return djangoGettext(param)
}
}
},
},
}
export function djangoGettext(param) {
@@ -86,8 +111,8 @@ export function djangoGettext(param) {
}
/*
* Utility function to use djangos named urls
* */
* Utility function to use djangos named urls
* */
// uses https://github.com/ierror/django-js-reverse#use-the-urls-in-javascript
export const ResolveUrlMixin = {
@@ -99,50 +124,48 @@ export const ResolveUrlMixin = {
*/
resolveDjangoUrl: function (url, params = null) {
return resolveDjangoUrl(url, params)
}
}
},
},
}
export function resolveDjangoUrl(url, params = null) {
if (params == null) {
return window.Urls[url]()
} else if (typeof(params) != "object") {
} else if (typeof params != "object") {
return window.Urls[url](params)
} else if (typeof(params) == "object") {
} else if (typeof params == "object") {
if (params.length === 1) {
return window.Urls[url](params)
} else if (params.length === 2) {
return window.Urls[url](params[0],params[1])
return window.Urls[url](params[0], params[1])
} else if (params.length === 3) {
return window.Urls[url](params[0],params[1],params[2])
return window.Urls[url](params[0], params[1], params[2])
}
}
}
/*
* other utilities
* */
* other utilities
* */
export function getUserPreference(pref) {
if(window.USER_PREF === undefined) {
return undefined;
if (window.USER_PREF === undefined) {
return undefined
}
return window.USER_PREF[pref]
}
import {frac} from "@/utils/fractions";
export function calculateAmount(amount, factor) {
if (getUserPreference('use_fractions')) {
let return_string = ''
let fraction = frac((amount * factor), 10, true)
if (getUserPreference("use_fractions")) {
let return_string = ""
let fraction = frac(amount * factor, 10, true)
if (fraction[0] > 0) {
return_string += fraction[0]
}
if (fraction[1] > 0) {
return_string += ` <sup>${(fraction[1])}</sup>&frasl;<sub>${(fraction[2])}</sub>`
return_string += ` <sup>${fraction[1]}</sup>&frasl;<sub>${fraction[2]}</sub>`
}
return return_string
@@ -152,23 +175,23 @@ export function calculateAmount(amount, factor) {
}
export function roundDecimals(num) {
let decimals = ((getUserPreference('user_fractions')) ? getUserPreference('user_fractions') : 2);
return +(Math.round(num + `e+${decimals}`) + `e-${decimals}`);
let decimals = getUserPreference("user_fractions") ? getUserPreference("user_fractions") : 2
return +(Math.round(num + `e+${decimals}`) + `e-${decimals}`)
}
const KILOJOULES_PER_CALORIE = 4.18
export function calculateEnergy(amount, factor) {
if (getUserPreference('use_kj')) {
if (getUserPreference("use_kj")) {
let joules = amount * KILOJOULES_PER_CALORIE
return calculateAmount(joules, factor) + ' kJ'
return calculateAmount(joules, factor) + " kJ"
} else {
return calculateAmount(amount, factor) + ' kcal'
return calculateAmount(amount, factor) + " kcal"
}
}
export function convertEnergyToCalories(amount) {
if (getUserPreference('use_kj')) {
if (getUserPreference("use_kj")) {
return amount / KILOJOULES_PER_CALORIE
} else {
return amount
@@ -176,33 +199,25 @@ export function convertEnergyToCalories(amount) {
}
export function energyHeading() {
if (getUserPreference('use_kj')) {
return 'Energy'
if (getUserPreference("use_kj")) {
return "Energy"
} else {
return 'Calories'
return "Calories"
}
}
/*
* Utility functions to use OpenAPIs generically
* */
import {ApiApiFactory} from "@/utils/openapi/api.ts";
import axios from "axios";
axios.defaults.xsrfCookieName = 'csrftoken'
axios.defaults.xsrfCookieName = "csrftoken"
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
import { Actions, Models } from './models';
import {RequestArgs} from "@/utils/openapi/base";
export const ApiMixin = {
data() {
return {
Models: Models,
Actions: Actions
Actions: Actions,
}
},
methods: {
genericAPI: function(model, action, options) {
genericAPI: function (model, action, options) {
let setup = getConfig(model, action)
if (setup?.config?.function) {
return specialCases[setup.config.function](action, options, setup)
@@ -212,10 +227,10 @@ export const ApiMixin = {
let apiClient = new ApiApiFactory()
return apiClient[func](...parameters)
},
genericGetAPI: function(url, options) {
return axios.get(this.resolveDjangoUrl(url), {'params':options, 'emulateJSON': true})
}
}
genericGetAPI: function (url, options) {
return axios.get(this.resolveDjangoUrl(url), { params: options, emulateJSON: true })
},
},
}
// /*
@@ -223,37 +238,37 @@ export const ApiMixin = {
// * */
function formatParam(config, value, options) {
if (config) {
for (const [k, v] of Object.entries(config)) {
switch(k) {
case 'type':
switch(v) {
case 'string':
for (const [k, v] of Object.entries(config)) {
switch (k) {
case "type":
switch (v) {
case "string":
if (Array.isArray(value)) {
let tmpValue = []
value.forEach(x => tmpValue.push(String(x)))
value.forEach((x) => tmpValue.push(String(x)))
value = tmpValue
} else if (value !== undefined) {
value = String(value)
}
break;
case 'integer':
break
case "integer":
if (Array.isArray(value)) {
let tmpValue = []
value.forEach(x => tmpValue.push(parseInt(x)))
value.forEach((x) => tmpValue.push(parseInt(x)))
value = tmpValue
} else if (value !== undefined) {
value = parseInt(value)
}
break;
break
}
break;
case 'function':
break
case "function":
// needs wrapped in a promise and wait for the called function to complete before moving on
specialCases[v](value, options)
break;
break
}
}
}
}
return value
}
function buildParams(options, setup) {
@@ -280,60 +295,56 @@ function buildParams(options, setup) {
this_value = getDefault(config?.[item], options)
}
parameters.push(this_value)
});
})
return parameters
}
function getDefault(config, options) {
let value = undefined
value = config?.default ?? undefined
if (typeof(value) === 'object') {
if (typeof value === "object") {
let condition = false
switch(value.function) {
switch (value.function) {
// CONDITIONAL case requires 4 keys:
// - check: which other OPTIONS key to check against
// - operator: what type of operation to perform
// - true: what value to assign when true
// - false: what value to assign when false
case 'CONDITIONAL':
switch(value.operator) {
case 'not_exist':
condition = (
(!options?.[value.check] ?? undefined)
|| options?.[value.check]?.length == 0
)
case "CONDITIONAL":
switch (value.operator) {
case "not_exist":
condition = (!options?.[value.check] ?? undefined) || options?.[value.check]?.length == 0
if (condition) {
value = value.true
} else {
value = value.false
}
break;
break
}
break;
break
}
}
return value
}
export function getConfig(model, action) {
let f = action.function
// if not defined partialUpdate will use params from create
if (f === 'partialUpdate' && !model?.[f]?.params) {
model[f] = {'params': [...['id'], ...model.create.params]}
if (f === "partialUpdate" && !model?.[f]?.params) {
model[f] = { params: [...["id"], ...model.create.params] }
}
let config = {
'name': model.name,
'apiName': model.apiName,
name: model.name,
apiName: model.apiName,
}
// spread operator merges dictionaries - last item in list takes precedence
config = {...config, ...action, ...model.model_type?.[f], ...model?.[f]}
config = { ...config, ...action, ...model.model_type?.[f], ...model?.[f] }
// nested dictionaries are not merged - so merge again on any nested keys
config.config = {...action?.config, ...model.model_type?.[f]?.config, ...model?.[f]?.config}
config.config = { ...action?.config, ...model.model_type?.[f]?.config, ...model?.[f]?.config }
// look in partialUpdate again if necessary
if (f === 'partialUpdate' && Object.keys(config.config).length === 0) {
config.config = {...model.model_type?.create?.config, ...model?.create?.config}
if (f === "partialUpdate" && Object.keys(config.config).length === 0) {
config.config = { ...model.model_type?.create?.config, ...model?.create?.config }
}
config['function'] = f + config.apiName + (config?.suffix ?? '') // parens are required to force optional chaining to evaluate before concat
config["function"] = f + config.apiName + (config?.suffix ?? "") // parens are required to force optional chaining to evaluate before concat
return config
}
@@ -342,181 +353,175 @@ export function getConfig(model, action) {
// * */
export function getForm(model, action, item1, item2) {
let f = action.function
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) {
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}
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) {
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 }
}
let form = {'fields': []}
let value = ''
let form = { fields: [] }
let value = ""
for (const [k, v] of Object.entries(config)) {
if (v?.function){
switch(v.function) {
case 'translate':
if (v?.function) {
switch (v.function) {
case "translate":
value = formTranslate(v, model, item1, item2)
}
} else {
value = v
}
if (value?.form_field) {
value['value'] = item1?.[value?.field] ?? undefined
form.fields.push(
{
...value,
...{
'label': formTranslate(value?.label, model, item1, item2),
'placeholder': formTranslate(value?.placeholder, model, item1, item2)
}
}
)
value["value"] = item1?.[value?.field] ?? undefined
form.fields.push({
...value,
...{
label: formTranslate(value?.label, model, item1, item2),
placeholder: formTranslate(value?.placeholder, model, item1, item2),
},
})
} else {
form[k] = value
}
}
return form
}
function formTranslate(translate, model, item1, item2) {
if (typeof(translate) !== 'object') {return translate}
if (typeof translate !== "object") {
return translate
}
let phrase = translate.phrase
let options = {}
let obj = undefined
translate?.params.forEach(function (x, index) {
switch(x.from){
case 'item1':
switch (x.from) {
case "item1":
obj = item1
break;
case 'item2':
break
case "item2":
obj = item2
break;
case 'model':
break
case "model":
obj = model
}
options[x.token] = obj[x.attribute]
})
return i18n.t(phrase, options)
}
// /*
// * Utility functions to use manipulate nested components
// * */
import Vue from 'vue'
export const CardMixin = {
methods: {
findCard: function(id, card_list){
findCard: function (id, card_list) {
let card_length = card_list?.length ?? 0
if (card_length == 0) {
return false
return false
}
let cards = card_list.filter(obj => obj.id == id)
let cards = card_list.filter((obj) => obj.id == id)
if (cards.length == 1) {
return cards[0]
return cards[0]
} else if (cards.length == 0) {
for (const c of card_list.filter(x => x.show_children == true)) {
cards = this.findCard(id, c.children)
if (cards) {
return cards
for (const c of card_list.filter((x) => x.show_children == true)) {
cards = this.findCard(id, c.children)
if (cards) {
return cards
}
}
}
} else {
console.log('something terrible happened')
console.log("something terrible happened")
}
},
destroyCard: function(id, card_list) {
destroyCard: function (id, card_list) {
let card = this.findCard(id, card_list)
let p_id = card?.parent ?? undefined
if (p_id) {
let parent = this.findCard(p_id, card_list)
if (parent){
Vue.set(parent, 'numchild', parent.numchild - 1)
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))
let idx = parent.children.indexOf(parent.children.find((x) => x.id === id))
Vue.delete(parent.children, idx)
}
}
}
return card_list.filter(x => x.id != id)
},
refreshCard: function(obj, card_list){
return card_list.filter((x) => x.id != id)
},
refreshCard: function (obj, card_list) {
let target = {}
let idx = undefined
target = this.findCard(obj.id, card_list)
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)
}
if (target?.parent) {
let parent = this.findCard(target.parent, card_list)
if (parent) {
if (parent.show_children){
idx = parent.children.indexOf(parent.children.find(x => x.id === target.id))
if (parent.show_children) {
idx = parent.children.indexOf(parent.children.find((x) => x.id === target.id))
Vue.set(parent.children, idx, obj)
}
}
}
},
}
},
}
const specialCases = {
// the supermarket API requires chaining promises together, instead of trying to make
// this use case generic just treat it as a unique use case
SupermarketWithCategories: function(action, options, setup) {
SupermarketWithCategories: function (action, options, setup) {
let API = undefined
let GenericAPI = ApiMixin.methods.genericAPI
let params = []
if (action.function === 'partialUpdate') {
if (action.function === "partialUpdate") {
API = GenericAPI
params = [Models.SUPERMARKET, Actions.FETCH, {'id': options.id}]
} else if (action.function === 'create') {
params = [Models.SUPERMARKET, Actions.FETCH, { id: options.id }]
} else if (action.function === "create") {
API = new ApiApiFactory()[setup.function]
params = buildParams(options, setup)
}
return API(...params).then((result) => {
// either get the supermarket or create the supermarket (but without the category relations)
return result.data
}).then((result) => {
// delete, update or change all of the category/relations
let id = result.id
let existing_categories = result.category_to_supermarket
let updated_categories = options.category_to_supermarket
let promises = []
// if the 'category.name' key does not exist on the updated_categories, the categories were not updated
if (updated_categories?.[0]?.category?.name) {
// list of category relationship ids that are not part of the updated supermarket
let removed_categories = existing_categories.filter(x => !updated_categories.map(x => x.category.id).includes(x.category.id))
let added_categories = updated_categories.filter(x => !existing_categories.map(x => x.category.id).includes(x.category.id))
let changed_categories = updated_categories.filter(x => existing_categories.map(x => x.category.id).includes(x.category.id))
removed_categories.forEach(x => {
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.DELETE, {'id': x.id}))
})
let item = {'supermarket': id}
added_categories.forEach(x => {
item.order = x.order
item.category = {'id': x.category.id, 'name': x.category.name}
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.CREATE, item))
})
changed_categories.forEach(x => {
item.id = x?.id ?? existing_categories.find(y => y.category.id === x.category.id).id;
item.order = x.order
item.category = {'id': x.category.id, 'name': x.category.name}
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.UPDATE, item))
})
}
return Promise.all(promises).then(() => {
// finally get and return the Supermarket which everything downstream is expecting
return GenericAPI(Models.SUPERMARKET, Actions.FETCH, {'id': id})
return API(...params)
.then((result) => {
// either get the supermarket or create the supermarket (but without the category relations)
return result.data
})
})
}
.then((result) => {
// delete, update or change all of the category/relations
let id = result.id
let existing_categories = result.category_to_supermarket
let updated_categories = options.category_to_supermarket
let promises = []
// if the 'category.name' key does not exist on the updated_categories, the categories were not updated
if (updated_categories?.[0]?.category?.name) {
// list of category relationship ids that are not part of the updated supermarket
let removed_categories = existing_categories.filter((x) => !updated_categories.map((x) => x.category.id).includes(x.category.id))
let added_categories = updated_categories.filter((x) => !existing_categories.map((x) => x.category.id).includes(x.category.id))
let changed_categories = updated_categories.filter((x) => existing_categories.map((x) => x.category.id).includes(x.category.id))
removed_categories.forEach((x) => {
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.DELETE, { id: x.id }))
})
let item = { supermarket: id }
added_categories.forEach((x) => {
item.order = x.order
item.category = { id: x.category.id, name: x.category.name }
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.CREATE, item))
})
changed_categories.forEach((x) => {
item.id = x?.id ?? existing_categories.find((y) => y.category.id === x.category.id).id
item.order = x.order
item.category = { id: x.category.id, name: x.category.name }
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.UPDATE, item))
})
}
return Promise.all(promises).then(() => {
// finally get and return the Supermarket which everything downstream is expecting
return GenericAPI(Models.SUPERMARKET, Actions.FETCH, { id: id })
})
})
},
}