Merge branch 'pr/897' into develop

This commit is contained in:
vabene1111 2021-09-15 09:16:09 +02:00
commit aeb5682176
20 changed files with 1626 additions and 126 deletions

View File

@ -1 +1 @@
.touchable[data-v-18b1d8a0]{padding-right:2em;padding-left:2em;margin-right:-2em;margin-left:-2em}.shake[data-v-94120e12]{-webkit-animation:shake-data-v-94120e12 .82s cubic-bezier(.36,.07,.19,.97) both;animation:shake-data-v-94120e12 .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-94120e12{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-94120e12{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)}}
.touchable[data-v-18b1d8a0]{padding-right:2em;padding-left:2em;margin-right:-2em;margin-left:-2em}.shake[data-v-e878d6ac]{-webkit-animation:shake-data-v-e878d6ac .82s cubic-bezier(.36,.07,.19,.97) both;animation:shake-data-v-e878d6ac .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-e878d6ac{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-e878d6ac{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

1391
vue/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -38,10 +38,11 @@
<!-- 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')"/>
</div>
: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"
@ -53,7 +54,8 @@
v-for="i in items_left" v-bind:key="i.id"
:item=i
:model="this_model"
@item-action="startAction($event, 'left')"/>
@item-action="startAction($event, 'left')"
@finish-action="finishAction"/>
</template>
</generic-infinite-cards>
</div>
@ -68,7 +70,8 @@
v-for="i in items_right" v-bind:key="i.id"
:item=i
:model="this_model"
@item-action="startAction($event, 'right')"/>
@item-action="startAction($event, 'right')"
@finish-action="finishAction"/>
</template>
</generic-infinite-cards>
</div>
@ -197,6 +200,11 @@ export default {
},
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:
@ -256,7 +264,11 @@ export default {
})
} else {
this.genericAPI(this.this_model, this.Actions.UPDATE, thisItem).then((result) => {
this.refreshThis(thisItem.id)
// using form data to refresh the card
// when there are complicated functions (SuperMarket Relations) the actions don't
// always complete first. TODO: wrap all that in a Promise and wait for it to complete before using refreshThis instead
this.refreshCard(thisItem, this.items_left)
this.refreshCard({...thisItem}, this.items_right)
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE)
}).catch((err) => {
console.log(err, err.response)

View File

@ -1,14 +1,15 @@
<template>
<div row style="margin: 4px">
<!-- @[useDrag&&`dragover`] <== this syntax completely shuts off draggable -->
<b-card no-body d-flex flex-column :class="{'border border-primary' : over, 'shake': isError}"
:style="{'cursor:grab' : useDrag}"
@dragover.prevent
@dragenter.prevent
:draggable="useDrag"
@dragstart="handleDragStart($event)"
@dragenter="handleDragEnter($event)"
@dragleave="handleDragLeave($event)"
@drop="handleDragDrop($event)">
@[useDrag&&`dragover`].prevent
@[useDrag&&`dragenter`].prevent
@[useDrag&&`dragstart`]="handleDragStart($event)"
@[useDrag&&`dragenter`]="handleDragEnter($event)"
@[useDrag&&`dragleave`]="handleDragLeave($event)"
@[useDrag&&`drop`]="handleDragDrop($event)">
<b-row no-gutters >
<b-col no-gutters class="col-sm-3">
<b-card-img-lazy style="object-fit: cover; height: 6em;" :src="item_image" v-bind:alt="$t('Recipe_Image')"></b-card-img-lazy>
@ -23,6 +24,13 @@
:item_list="item[x.field]"
:label="x.label"
:color="x.color"/>
<generic-ordered-pill v-for="x in itemOrderedTags" :key="x.field"
:item_list="item[x.field]"
:label="x.label"
:color="x.color"
:field="x.field"
:item="item"
@finish-action="finishAction"/>
<div class="mt-auto mb-1" align="right">
<span v-if="item[child_count]" class="mx-2 btn btn-link btn-sm"
style="z-index: 800;" v-on:click="$emit('item-action',{'action':'get-children','source':item})">
@ -90,13 +98,14 @@
import GenericContextMenu from "@/components/GenericContextMenu";
import Badges from "@/components/Badges";
import GenericPill from "@/components/GenericPill";
import GenericOrderedPill from "@/components/GenericOrderedPill";
import RecipeCard from "@/components/RecipeCard";
import { mixin as clickaway } from 'vue-clickaway';
import { createPopper } from '@popperjs/core';
export default {
name: "GenericHorizontalCard",
components: { GenericContextMenu, RecipeCard, Badges, GenericPill},
components: { GenericContextMenu, RecipeCard, Badges, GenericPill, GenericOrderedPill},
mixins: [clickaway],
props: {
item: {type: Object},
@ -142,6 +151,9 @@ export default {
},
itemTags: function() {
return this.model?.tags ?? []
},
itemOrderedTags: function() {
return this.model?.ordered_tags ?? []
}
},
methods: {
@ -206,6 +218,10 @@ export default {
closeMenu: function(){
this.show_menu = false
},
finishAction: function(e){
this.$emit('finish-action', e)
}
}
}
</script>

View File

@ -0,0 +1,72 @@
<template>
<draggable v-if="itemList" v-model="this_list" tag="span" group="ordered_items" z-index="500"
@change="orderChanged">
<span :key="k.id" v-for="k in itemList" class="pl-1">
<b-badge pill :variant="color">{{thisLabel(k)}}</b-badge>
</span>
</draggable>
</template>
<script>
// you can't use this component with a horizontal card that is also draggable
import draggable from 'vuedraggable'
export default {
name: 'GenericOrderedPill',
components: {draggable},
props: {
item_list: {required: true, type: Array},
label: {type: String, default: 'name'},
color: {type: String, default: 'light'},
field: {type: String, required: true},
item: {type: Object},
},
data() {
return {
this_list: [],
}
},
computed: {
itemList: function() {
if(Array.isArray(this.this_list)) {
return this.this_list
} else if (!this.this_list?.name) {
return false
} else {
return [this.this_list]
}
},
},
mounted() {
this.this_list = this.item_list
},
watch: {
'item_list': function (newVal) {
this.this_list = newVal
}
},
methods: {
thisLabel: function (item) {
let fields = this.label.split('::')
let value = item
fields.forEach(x => {
value = value[x]
});
return value
},
orderChanged: function(e){
let order = 0
this.this_list.forEach(x => {
x['order'] = order
order++
})
let new_order = {...this.item}
new_order[this.field] = this.this_list
this.$emit('finish-action', {'action':'save','form_data': new_order })
},
}
}
</script>

View File

@ -6,12 +6,8 @@
<p v-if="f.type=='instruction'">{{f.label}}</p>
<!-- this lookup is single selection -->
<lookup-input v-if="f.type=='lookup'"
:label="f.label"
:value="f.value"
:field="f.field"
:form="f"
:model="listModel(f.list)"
:sticky_options="f.sticky_options || undefined"
:create_new="f.allow_create"
@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'"
@ -65,7 +61,8 @@ export default {
id: undefined,
form_data: {},
form: {},
dirty: false
dirty: false,
special_handling: false
}
},
mounted() {
@ -93,7 +90,7 @@ export default {
methods: {
doAction: function(){
this.dirty = false
this.$emit('finish-action', {'form_data': this.form_data })
this.$emit('finish-action', {'form_data': this.detectOverride(this.form_data) })
},
cancelAction: function() {
if (this.dirty) {
@ -110,6 +107,14 @@ export default {
} else {
return Models[m]
}
},
detectOverride: function(form) {
for (const [k, v] of Object.entries(form)) {
if (form[k].__override__) {
form[k] = form[k].__override__
}
}
return form
}
}
}

View File

@ -1,14 +1,14 @@
<template>
<div>
<b-form-group
v-bind:label="label"
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="false"
:multiple="useMultiple"
:sticky_options="sticky_options"
:allow_create="create_new"
:create_placeholder="createPlaceholder"
@ -29,12 +29,9 @@ export default {
components: {GenericMultiselect},
mixins: [ApiMixin],
props: {
field: {type: String, default: 'You Forgot To Set Field Name'},
label: {type: String, default: ''},
value: {type: Object, default () {return undefined}},
form: {type: Object, default () {return undefined}},
model: {type: Object, default () {return undefined}},
create_new: {type: Boolean, default: false},
sticky_options: {type:Array, default(){return []}},
// 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
@ -42,25 +39,43 @@ export default {
data() {
return {
new_value: undefined,
field: undefined,
label: undefined,
sticky_options: undefined,
first_run: true
}
},
mounted() {
this.new_value = this.value
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.form.value
let arrayValues = undefined
// multiselect is expect to get an array of objects - make sure it gets one
if (Array.isArray(this.new_value)) {
return this.new_value
} else if (!this.new_value) {
return []
} else if (typeof(this.new_value) === 'object') {
return [this.new_value]
if (Array.isArray(this_value)) {
arrayValues = this_value
} else if (!this_value) {
arrayValues = []
} else if (typeof(this_value) === 'object') {
arrayValues = [this_value]
} else {
return [{'id': -1, 'name': this.new_value}]
arrayValues = [{'id': -1, 'name': this_value}]
}
if (this.form?.ordered && this.first_run) {
return this.flattenItems(arrayValues)
} else {
return arrayValues
}
},
createPlaceholder() {
@ -69,7 +84,10 @@ export default {
},
watch: {
'new_value': function () {
this.$root.$emit('change', this.field, this.new_value ?? null)
let x = this?.new_value
// pass the unflattened attributes that can be restored when ready to save/update
x['__override__'] = this.unflattenItem(this?.new_value)
this.$root.$emit('change', this.form.field, x)
},
},
methods: {
@ -84,6 +102,54 @@ export default {
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>

View File

@ -229,8 +229,8 @@ export class Models {
},
}
static SHOPPING_CATEGORY_RELATION = {
'name': i18n.t('Shopping_Category'),
'apiName': 'SupermarketCategory',
'name': i18n.t('Shopping_Category_Relation'),
'apiName': 'SupermarketCategoryRelation',
'create': {
'params': [['category', 'supermarket', 'order']],
'form': {
@ -254,7 +254,7 @@ export class Models {
static SUPERMARKET = {
'name': i18n.t('Supermarket'),
'apiName': 'Supermarket',
'tags': [{'field': 'category_to_supermarket', 'label': 'category::name', 'color': 'info'}],
'ordered_tags': [{'field': 'category_to_supermarket', 'label': 'category::name', 'color': 'info'}],
'create': {
'params': [['name', 'description', 'category_to_supermarket']],
'form': {
@ -272,6 +272,19 @@ export class Models {
'label': i18n.t('Description'),
'placeholder': ''
},
'categories': {
'form_field': true,
'type': 'lookup',
'list': 'SHOPPING_CATEGORY',
'list_label': 'category::name',
'ordered': true, // ordered lookups assume working with relation field
'field': 'category_to_supermarket',
'label': i18n.t('Categories'),
'placeholder': ''
},
},
'config': {
'category_to_supermarket': {'function': 'handleSuperMarketCategory'}
}
},
}

View File

@ -189,11 +189,11 @@ export const ApiMixin = {
// maybe map/reduce is better?
for (const [k, v] of Object.entries(options)) {
if (item.includes(k)) {
this_value[k] = formatParam(config?.[k], v)
this_value[k] = formatParam(config?.[k], v, options)
}
}
} else {
this_value = formatParam(config?.[item], options?.[item] ?? undefined)
this_value = formatParam(config?.[item], options?.[item] ?? undefined, options)
}
// if no value is found so far, get the default if it exists
if (this_value === undefined) {
@ -210,7 +210,7 @@ export const ApiMixin = {
// /*
// * local functions for ApiMixin
// * */
function formatParam(config, value) {
function formatParam(config, value, options) {
if (config) {
for (const [k, v] of Object.entries(config)) {
switch(k) {
@ -236,6 +236,10 @@ function formatParam(config, value) {
break;
}
break;
case 'function':
// needs wrapped in a promise and wait for the called function to complete before moving on
specialCases[v](value, options)
break;
}
}
}
@ -272,6 +276,7 @@ function getDefault(config, options) {
return value
}
function getConfig(model, action) {
let f = action.function
// if not defined partialUpdate will use params from create
if (f === 'partialUpdate' && !model?.[f]?.params) {
@ -286,6 +291,10 @@ function getConfig(model, action) {
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}
// 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}
}
config['function'] = f + config.apiName + (config?.suffix ?? '') // parens are required to force optional chaining to evaluate before concat
return config
}
@ -415,3 +424,31 @@ export const CardMixin = {
}
}
const specialCases = {
handleSuperMarketCategory: function(updatedRelationships, supermarket) {
let API = ApiMixin.methods.genericAPI
if (updatedRelationships.length === 0) {
return
}
// get current relationship mappings
API(Models.SUPERMARKET, Actions.FETCH, {'id': supermarket.id}).then((result) => {
let currentRelationships = result.data.category_to_supermarket
let removed = currentRelationships.map(x => x.id).filter(x => !updatedRelationships.map(x => x.id).includes(x))
removed.forEach(x => {
API(Models.SHOPPING_CATEGORY_RELATION, Actions.DELETE, {'id': x})//.then((result)=> console.log('delete', result))
})
let item = {'supermarket': supermarket.id}
updatedRelationships.forEach(x => {
item.order = x.order
item.category = {'id': x.category.id, 'name': x.category.name}
if (x.id) {
item.id = x.id
API(Models.SHOPPING_CATEGORY_RELATION, Actions.UPDATE, item)//.then((result)=> console.log('update', result))
} else {
API(Models.SHOPPING_CATEGORY_RELATION, Actions.CREATE, item)//.then((result)=> console.log('create', result))
}
})
})
}
}

View File

@ -3,35 +3,35 @@
"assets": {
"../../templates/sw.js": {
"name": "../../templates/sw.js",
"path": "..\\..\\templates\\sw.js"
"path": "../../templates/sw.js"
},
"css/chunk-vendors.css": {
"name": "css/chunk-vendors.css",
"path": "css\\chunk-vendors.css"
"path": "css/chunk-vendors.css"
},
"js/chunk-vendors.js": {
"name": "js/chunk-vendors.js",
"path": "js\\chunk-vendors.js"
"path": "js/chunk-vendors.js"
},
"css/cookbook_view.css": {
"name": "css/cookbook_view.css",
"path": "css\\cookbook_view.css"
"path": "css/cookbook_view.css"
},
"js/cookbook_view.js": {
"name": "js/cookbook_view.js",
"path": "js\\cookbook_view.js"
"path": "js/cookbook_view.js"
},
"css/edit_internal_recipe.css": {
"name": "css/edit_internal_recipe.css",
"path": "css\\edit_internal_recipe.css"
"path": "css/edit_internal_recipe.css"
},
"js/edit_internal_recipe.js": {
"name": "js/edit_internal_recipe.js",
"path": "js\\edit_internal_recipe.js"
"path": "js/edit_internal_recipe.js"
},
"js/import_response_view.js": {
"name": "js/import_response_view.js",
"path": "js\\import_response_view.js"
"path": "js/import_response_view.js"
},
"css/meal_plan_view.css": {
"name": "css/meal_plan_view.css",
@ -43,39 +43,39 @@
},
"css/model_list_view.css": {
"name": "css/model_list_view.css",
"path": "css\\model_list_view.css"
"path": "css/model_list_view.css"
},
"js/model_list_view.js": {
"name": "js/model_list_view.js",
"path": "js\\model_list_view.js"
"path": "js/model_list_view.js"
},
"js/offline_view.js": {
"name": "js/offline_view.js",
"path": "js\\offline_view.js"
"path": "js/offline_view.js"
},
"css/recipe_search_view.css": {
"name": "css/recipe_search_view.css",
"path": "css\\recipe_search_view.css"
"path": "css/recipe_search_view.css"
},
"js/recipe_search_view.js": {
"name": "js/recipe_search_view.js",
"path": "js\\recipe_search_view.js"
"path": "js/recipe_search_view.js"
},
"css/recipe_view.css": {
"name": "css/recipe_view.css",
"path": "css\\recipe_view.css"
"path": "css/recipe_view.css"
},
"js/recipe_view.js": {
"name": "js/recipe_view.js",
"path": "js\\recipe_view.js"
"path": "js/recipe_view.js"
},
"js/supermarket_view.js": {
"name": "js/supermarket_view.js",
"path": "js\\supermarket_view.js"
"path": "js/supermarket_view.js"
},
"js/user_file_view.js": {
"name": "js/user_file_view.js",
"path": "js\\user_file_view.js"
"path": "js/user_file_view.js"
},
"recipe_search_view.html": {
"name": "recipe_search_view.html",