add new from ListInput

This commit is contained in:
smilerz 2021-09-08 14:01:21 -05:00
parent 04c047bd31
commit 83a747739a
17 changed files with 173 additions and 103 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -259,7 +259,7 @@ else:
'USER': os.getenv('POSTGRES_USER'), 'USER': os.getenv('POSTGRES_USER'),
'PASSWORD': os.getenv('POSTGRES_PASSWORD'), 'PASSWORD': os.getenv('POSTGRES_PASSWORD'),
'NAME': os.getenv('POSTGRES_DB') if os.getenv('POSTGRES_DB') else 'db.sqlite3', 'NAME': os.getenv('POSTGRES_DB') if os.getenv('POSTGRES_DB') else 'db.sqlite3',
'CONN_MAX_AGE': 600, 'CONN_MAX_AGE': 60,
} }
} }

View File

@ -64,7 +64,7 @@ import {BootstrapVue} from 'bootstrap-vue'
import 'bootstrap-vue/dist/bootstrap-vue.css' import 'bootstrap-vue/dist/bootstrap-vue.css'
import {CardMixin, ToastMixin, ApiMixin} from "@/utils/utils"; import {CardMixin, ApiMixin} from "@/utils/utils";
import {StandardToasts} from "@/utils/utils"; import {StandardToasts} from "@/utils/utils";
import GenericSplitLists from "@/components/GenericSplitLists"; import GenericSplitLists from "@/components/GenericSplitLists";
@ -77,7 +77,7 @@ export default {
// TODO ApiGenerator doesn't capture and share error information - would be nice to share error details when available // TODO ApiGenerator doesn't capture and share error information - would be nice to share error details when available
// or i'm capturing it incorrectly // or i'm capturing it incorrectly
name: 'ModelListView', name: 'ModelListView',
mixins: [CardMixin, ToastMixin, ApiMixin], mixins: [CardMixin, ApiMixin],
components: {GenericHorizontalCard, GenericSplitLists, GenericModalForm}, components: {GenericHorizontalCard, GenericSplitLists, GenericModalForm},
data() { data() {
return { return {

View File

@ -10,11 +10,12 @@
:label="label" :label="label"
track-by="id" track-by="id"
:multiple="multiple" :multiple="multiple"
:taggable="create_new" :taggable="allow_create"
:tag-placeholder="createText" :tag-placeholder="create_placeholder"
:loading="loading" :loading="loading"
@search-change="search" @search-change="search"
@input="selectionChanged"> @input="selectionChanged"
@tag="addNew">
</multiselect> </multiselect>
</template> </template>
@ -44,33 +45,22 @@ export default {
sticky_options: {type:Array, default(){return []}}, sticky_options: {type:Array, default(){return []}},
initial_selection: {type:Array, default(){return []}}, initial_selection: {type:Array, default(){return []}},
multiple: {type: Boolean, default: true}, multiple: {type: Boolean, default: true},
create_new: {type: Boolean, default: false}, // TODO: this will create option to add new drop-downs allow_create: {type: Boolean, default: false}, // TODO: this will create option to add new drop-downs
create_text: {type: String, default: 'You Forgot to Add a Tag Placeholder'}, create_placeholder: {type: String, default: 'You Forgot to Add a Tag Placeholder'},
}, },
watch: { watch: {
initial_selection: function (newVal, oldVal) { // watch it initial_selection: function (newVal, oldVal) { // watch it
if (this.multiple) { this.selected_objects = newVal
this.selected_objects = newVal
} else if (this.selected_objects != newVal?.[0]) {
// when not using multiple selections need to convert array to value
this.selected_objects = newVal?.[0] ?? null
}
}, },
}, },
mounted() { mounted() {
this.search('') this.search('')
// when not using multiple selections need to convert array to value this.selected_objects = this.initial_selection
if (!this.multiple & this.selected_objects != this.initial_selection?.[0]) {
this.selected_objects = this.initial_selection?.[0] ?? null
}
}, },
computed: { computed: {
lookupPlaceholder() { lookupPlaceholder() {
return this.placeholder || this.model.name || this.$t('Search') return this.placeholder || this.model.name || this.$t('Search')
}, },
createText() {
return this.create_text
},
}, },
methods: { methods: {
// this.genericAPI inherited from ApiMixin // this.genericAPI inherited from ApiMixin
@ -85,7 +75,19 @@ export default {
}) })
}, },
selectionChanged: function () { selectionChanged: function () {
this.$emit('change', {var: this.parent_variable, val: this.selected_objects}) if (this.multiple) {
this.$emit('change', {var: this.parent_variable, val: this.selected_objects})
} else {
// if not multiple listener is expecting a single object, not an array
this.$emit('change', {var: this.parent_variable, val: this.selected_objects?.[0] ?? null})
}
},
addNew(e) {
this.$emit('new', e)
// could refactor as Promise - seems unecessary
setTimeout(() => { this.search(''); }, 750);
} }
} }
} }

View File

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<b-modal class="modal" id="modal" @hidden="cancelAction"> <b-modal :id="'modal_'+id" @hidden="cancelAction">
<template v-slot:modal-title><h4>{{form.title}}</h4></template> <template v-slot:modal-title><h4>{{form.title}}</h4></template>
<div v-for="(f, i) in form.fields" v-bind:key=i> <div v-for="(f, i) in form.fields" v-bind:key=i>
<p v-if="f.type=='instruction'">{{f.label}}</p> <p v-if="f.type=='instruction'">{{f.label}}</p>
@ -11,6 +11,7 @@
:field="f.field" :field="f.field"
:model="listModel(f.list)" :model="listModel(f.list)"
:sticky_options="f.sticky_options || undefined" :sticky_options="f.sticky_options || undefined"
:create_new="f.allow_create"
@change="storeValue"/> <!-- TODO add ability to create new items associated with lookup --> @change="storeValue"/> <!-- TODO add ability to create new items associated with lookup -->
<!-- TODO: add multi-selection input list --> <!-- TODO: add multi-selection input list -->
<checkbox-input v-if="f.type=='checkbox'" <checkbox-input v-if="f.type=='checkbox'"
@ -53,21 +54,24 @@ export default {
name: 'GenericModalForm', name: 'GenericModalForm',
components: {CheckboxInput, LookupInput, TextInput, EmojiInput}, components: {CheckboxInput, LookupInput, TextInput, EmojiInput},
props: { props: {
model: {required: true, type: Object, default: function() {}}, model: {required: true, type: Object},
action: {required: true, type: Object, default: function() {}}, action: {required: true, type: Object},
item1: {type: Object, default: function() {}}, item1: {type: Object, default () {return undefined}},
item2: {type: Object, default: function() {}}, item2: {type: Object, default () {return undefined}},
show: {required: true, type: Boolean, default: false}, show: {required: true, type: Boolean, default: false},
}, },
data() { data() {
return { return {
id: undefined,
form_data: {}, form_data: {},
form: {}, form: {},
dirty: false dirty: false
} }
}, },
mounted() { mounted() {
this.$root.$on('change', this.storeValue); // modal is outside Vue instance(?) so have to listen at root of component this.id = Math.random()
this.$root.$on('change', this.storeValue); // boostrap modal placed at document so have to listen at root of component
}, },
computed: { computed: {
buttonLabel() { buttonLabel() {
@ -79,9 +83,9 @@ export default {
if (this.show) { if (this.show) {
this.form = getForm(this.model, this.action, this.item1, this.item2) this.form = getForm(this.model, this.action, this.item1, this.item2)
this.dirty = true this.dirty = true
this.$bvModal.show('modal') this.$bvModal.show('modal_' + this.id)
} else { } else {
this.$bvModal.hide('modal') this.$bvModal.hide('modal_' + this.id)
this.form_data = {} this.form_data = {}
} }
}, },

View File

@ -1,16 +1,20 @@
<template> <template>
<div> <div>
<b-form-group <b-form-group
v-bind:label="label" v-bind:label="label"
class="mb-3"> class="mb-3">
<generic-multiselect <generic-multiselect
@change="new_value=$event.val" @change="new_value=$event.val"
:initial_selection="[value]" @remove="new_value=undefined"
:initial_selection="initialSelection"
:model="model" :model="model"
:multiple="false" :multiple="false"
:sticky_options="sticky_options" :sticky_options="sticky_options"
:allow_create="create_new"
:create_placeholder="createPlaceholder"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0" style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
:placeholder="modelName"> :placeholder="modelName"
@new="addNew">
</generic-multiselect> </generic-multiselect>
</b-form-group> </b-form-group>
</div> </div>
@ -18,15 +22,18 @@
<script> <script>
import GenericMultiselect from "@/components/GenericMultiselect"; import GenericMultiselect from "@/components/GenericMultiselect";
import {StandardToasts, ApiMixin} from "@/utils/utils";
export default { export default {
name: 'LookupInput', name: 'LookupInput',
components: {GenericMultiselect}, components: {GenericMultiselect},
mixins: [ApiMixin],
props: { props: {
field: {type: String, default: 'You Forgot To Set Field Name'}, field: {type: String, default: 'You Forgot To Set Field Name'},
label: {type: String, default: ''}, label: {type: String, default: ''},
value: {type: Object, default () {return {}}}, value: {type: Object, default () {return undefined}},
model: {type: Object, default () {return {}}}, model: {type: Object, default () {return undefined}},
create_new: {type: Boolean, default: false},
sticky_options: {type:Array, default(){return []}}, sticky_options: {type:Array, default(){return []}},
// TODO: include create_new and create_text props and associated functionality to create objects for drop down // 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 // see 'tagging' here: https://vue-multiselect.js.org/#sub-tagging
@ -37,9 +44,27 @@ export default {
new_value: undefined, new_value: undefined,
} }
}, },
mounted() {
this.new_value = this.value
},
computed: { computed: {
modelName() { modelName() {
return this?.model?.name ?? this.$t('Search') return this?.model?.name ?? this.$t('Search')
},
initialSelection() {
// 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 (this.new_value.id) {
return [this.new_value]
} else {
return [{'id': -1, 'name': this.new_value}]
}
},
createPlaceholder() {
return this.$t('Create_New_' + this?.model?.name)
} }
}, },
watch: { watch: {
@ -47,5 +72,20 @@ export default {
this.$root.$emit('change', this.field, this.new_value ?? null) this.$root.$emit('change', this.field, this.new_value ?? null)
}, },
}, },
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)
})
this.show_modal = false
},
}
} }
</script> </script>

View File

@ -125,5 +125,9 @@
"Icon": "Icon", "Icon": "Icon",
"Unit": "Unit", "Unit": "Unit",
"No_Results": "No Results", "No_Results": "No Results",
"New_Unit": "New Unit" "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"
} }

View File

@ -100,6 +100,7 @@ export class Models {
'field': 'supermarket_category', 'field': 'supermarket_category',
'list': 'SHOPPING_CATEGORY', 'list': 'SHOPPING_CATEGORY',
'label': i18n.t('Shopping_Category'), 'label': i18n.t('Shopping_Category'),
'allow_create': true
}, },
} }
}, },
@ -167,7 +168,26 @@ export class Models {
} }
static SHOPPING_CATEGORY = { static SHOPPING_CATEGORY = {
'name': i18n.t('Shopping_Category'), 'name': i18n.t('Shopping_Category'),
'apiName': 'SupermarketCategory', 'apiName': 'SupermarketCategory',
'create': {
'params': [['name', 'description']],
'form': {
'name': {
'form_field': true,
'type': 'text',
'field': 'name',
'label': i18n.t('Name'),
'placeholder': ''
},
'description': {
'form_field': true,
'type': 'text',
'field': 'description',
'label': i18n.t('Description'),
'placeholder': ''
}
}
},
} }
static RECIPE = { static RECIPE = {

View File

@ -3,47 +3,47 @@
"assets": { "assets": {
"../../templates/sw.js": { "../../templates/sw.js": {
"name": "../../templates/sw.js", "name": "../../templates/sw.js",
"path": "..\\..\\templates\\sw.js" "path": "../../templates/sw.js"
}, },
"css/chunk-vendors.css": { "css/chunk-vendors.css": {
"name": "css/chunk-vendors.css", "name": "css/chunk-vendors.css",
"path": "css\\chunk-vendors.css" "path": "css/chunk-vendors.css"
}, },
"js/chunk-vendors.js": { "js/chunk-vendors.js": {
"name": "js/chunk-vendors.js", "name": "js/chunk-vendors.js",
"path": "js\\chunk-vendors.js" "path": "js/chunk-vendors.js"
}, },
"js/import_response_view.js": { "js/import_response_view.js": {
"name": "js/import_response_view.js", "name": "js/import_response_view.js",
"path": "js\\import_response_view.js" "path": "js/import_response_view.js"
}, },
"css/model_list_view.css": { "css/model_list_view.css": {
"name": "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": { "js/model_list_view.js": {
"name": "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": { "js/offline_view.js": {
"name": "js/offline_view.js", "name": "js/offline_view.js",
"path": "js\\offline_view.js" "path": "js/offline_view.js"
}, },
"js/recipe_search_view.js": { "js/recipe_search_view.js": {
"name": "js/recipe_search_view.js", "name": "js/recipe_search_view.js",
"path": "js\\recipe_search_view.js" "path": "js/recipe_search_view.js"
}, },
"js/recipe_view.js": { "js/recipe_view.js": {
"name": "js/recipe_view.js", "name": "js/recipe_view.js",
"path": "js\\recipe_view.js" "path": "js/recipe_view.js"
}, },
"js/supermarket_view.js": { "js/supermarket_view.js": {
"name": "js/supermarket_view.js", "name": "js/supermarket_view.js",
"path": "js\\supermarket_view.js" "path": "js/supermarket_view.js"
}, },
"js/user_file_view.js": { "js/user_file_view.js": {
"name": "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": { "recipe_search_view.html": {
"name": "recipe_search_view.html", "name": "recipe_search_view.html",