Expand the "product lookup" component to include autocomplete

This commit is contained in:
Lance Edgar 2023-10-25 20:10:21 -05:00
parent 441a6e5e0c
commit a812181466
2 changed files with 155 additions and 73 deletions

View file

@ -531,33 +531,10 @@
<p class="label control"> <p class="label control">
Product Product
</p> </p>
<b-field :expanded="!productUUID"> <tailbone-product-lookup ref="productLookup"
<tailbone-autocomplete ref="productAutocomplete" :selected-product="selectedProduct"
v-model="productUUID" @selected="productLookupSelected">
placeholder="Enter UPC or brand, description etc." </tailbone-product-lookup>
:assigned-label="productDisplay"
serviceUrl="${url('{}.product_autocomplete'.format(route_prefix))}"
@input="productChanged">
</tailbone-autocomplete>
</b-field>
<b-button type="is-primary"
v-if="!productUUID"
@click="productFullLookup()"
icon-pack="fas"
icon-left="search">
Full Lookup
</b-button>
<b-button v-if="productUUID"
type="is-primary"
tag="a" target="_blank"
:href="productURL"
:disabled="!productURL"
icon-pack="fas"
icon-left="external-link-alt">
View Product
</b-button>
</b-field> </b-field>
<div v-if="productUUID"> <div v-if="productUUID">
@ -565,7 +542,6 @@
<div class="is-pulled-right has-text-centered"> <div class="is-pulled-right has-text-centered">
<img :src="productImageURL" <img :src="productImageURL"
style="max-height: 150px; max-width: 150px; "/> style="max-height: 150px; max-width: 150px; "/>
## <p>{{ productKey }}</p>
</div> </div>
<b-field grouped> <b-field grouped>
@ -957,11 +933,6 @@
</b-modal> </b-modal>
% endif % endif
<tailbone-product-lookup ref="productLookup"
@canceled="productLookupCanceled"
@selected="productLookupSelected">
</tailbone-product-lookup>
% if allow_past_item_reorder: % if allow_past_item_reorder:
<b-modal :active.sync="pastItemsShowDialog"> <b-modal :active.sync="pastItemsShowDialog">
<div class="card"> <div class="card">
@ -1258,6 +1229,7 @@
pastItemsSelected: null, pastItemsSelected: null,
% endif % endif
productIsKnown: true, productIsKnown: true,
selectedProduct: null,
productUUID: null, productUUID: null,
productDisplay: null, productDisplay: null,
productKey: null, productKey: null,
@ -1544,6 +1516,18 @@
this.$refs.contactAutocomplete.clearSelection() this.$refs.contactAutocomplete.clearSelection()
} }
}, },
productIsKnown(newval, oldval) {
// TODO: seems like this should be better somehow?
// e.g. maybe we should not be clearing *everything*
// in case user accidentally clicks, and then clicks
// "is known" again? and if we *should* clear all,
// why does that require 2 steps?
if (!newval) {
this.selectedProduct = null
this.clearProduct()
}
},
}, },
methods: { methods: {
@ -1894,20 +1878,12 @@
} }
}, },
productFullLookup() {
this.showingItemDialog = false
let term = this.$refs.productAutocomplete.getUserInput()
this.$refs.productLookup.showDialog(term)
},
productLookupCanceled() {
this.showingItemDialog = true
},
productLookupSelected(selected) { productLookupSelected(selected) {
// TODO: this still is a hack somehow, am sure of it.
// need to clean this up at some point
this.selectedProduct = selected
this.clearProduct() this.clearProduct()
this.productChanged(selected.uuid) this.productChanged(selected)
this.showingItemDialog = true
}, },
copyPendingProductAttrs(from, to) { copyPendingProductAttrs(from, to) {
@ -1930,6 +1906,7 @@
this.customerPanelOpen = false this.customerPanelOpen = false
this.editingItem = null this.editingItem = null
this.productIsKnown = true this.productIsKnown = true
this.selectedProduct = null
this.productUUID = null this.productUUID = null
this.productDisplay = null this.productDisplay = null
this.productKey = null this.productKey = null
@ -1962,7 +1939,7 @@
this.itemDialogTabIndex = 0 this.itemDialogTabIndex = 0
this.showingItemDialog = true this.showingItemDialog = true
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.productAutocomplete.focus() this.$refs.productLookup.focus()
}) })
}, },
@ -2027,6 +2004,16 @@
this.productIsKnown = !!row.product_uuid this.productIsKnown = !!row.product_uuid
this.productUUID = row.product_uuid this.productUUID = row.product_uuid
if (row.product_uuid) {
this.selectedProduct = {
uuid: row.product_uuid,
full_description: row.product_full_description,
url: row.product_url,
}
} else {
this.selectedProduct = null
}
// nb. must construct new object before updating data // nb. must construct new object before updating data
// (otherwise vue does not notice the changes?) // (otherwise vue does not notice the changes?)
let pending = {} let pending = {}
@ -2131,11 +2118,11 @@
} }
}, },
productChanged(uuid) { productChanged(product) {
if (uuid) { if (product) {
let params = { let params = {
action: 'get_product_info', action: 'get_product_info',
uuid: uuid, uuid: product.uuid,
} }
// nb. it is possible for the handler to "swap" // nb. it is possible for the handler to "swap"
// the product selection, i.e. user chooses a "per // the product selection, i.e. user chooses a "per
@ -2144,6 +2131,8 @@
// received above is the correct one, but just use // received above is the correct one, but just use
// whatever came back from handler // whatever came back from handler
this.submitBatchData(params, response => { this.submitBatchData(params, response => {
this.selectedProduct = response.data
this.productUUID = response.data.uuid this.productUUID = response.data.uuid
this.productKey = response.data.key this.productKey = response.data.key
this.productDisplay = response.data.full_description this.productDisplay = response.data.full_description

View file

@ -2,8 +2,49 @@
<%def name="tailbone_product_lookup_template()"> <%def name="tailbone_product_lookup_template()">
<script type="text/x-template" id="tailbone-product-lookup-template"> <script type="text/x-template" id="tailbone-product-lookup-template">
<div> <div style="width: 100%;">
<b-modal :active.sync="showingDialog">
<b-field grouped>
<b-field :expanded="!selectedProduct">
<b-autocomplete ref="productAutocomplete"
v-if="!selectedProduct"
v-model="autocompleteValue"
placeholder="Enter UPC or brand, description etc."
:data="autocompleteOptions"
field="value"
:custom-formatter="option => option.label"
@typing="getAutocompleteOptions"
@select="autocompleteSelected"
style="width: 100%;">
</b-autocomplete>
<b-button v-if="selectedProduct"
@click="clearSelection(true)">
{{ selectedProduct.full_description }}
</b-button>
</b-field>
<b-button type="is-primary"
v-if="!selectedProduct"
@click="lookupInit()"
icon-pack="fas"
icon-left="search">
Full Lookup
</b-button>
<b-button v-if="selectedProduct"
type="is-primary"
tag="a" target="_blank"
:href="selectedProduct.url"
:disabled="!selectedProduct.url"
icon-pack="fas"
icon-left="external-link-alt">
View Product
</b-button>
</b-field>
<b-modal :active.sync="lookupShowDialog">
<div class="card"> <div class="card">
<div class="card-content"> <div class="card-content">
@ -157,6 +198,7 @@
</div> </div>
</div> </div>
</b-modal> </b-modal>
</div> </div>
</script> </script>
</%def> </%def>
@ -166,9 +208,17 @@
const TailboneProductLookup = { const TailboneProductLookup = {
template: '#tailbone-product-lookup-template', template: '#tailbone-product-lookup-template',
props: {
selectedProduct: {
type: Object,
},
},
data() { data() {
return { return {
showingDialog: false, autocompleteValue: '',
autocompleteOptions: [],
lookupShowDialog: false,
searchTerm: null, searchTerm: null,
searchTermLastUsed: null, searchTermLastUsed: null,
@ -187,23 +237,67 @@
}, },
methods: { methods: {
showDialog(term) { focus() {
if (!this.selectedProduct) {
this.$refs.productAutocomplete.focus()
}
},
clearSelection(focus) {
// clear data
this.autocompleteValue = ''
this.$emit('selected', null)
// maybe set focus to our (autocomplete) component
if (focus) {
this.$nextTick(() => {
this.focus()
})
}
},
getAutocompleteOptions: debounce(function (entry) {
// since the `@typing` event from buefy component does not
// "self-regulate" in any way, we a) use `debounce` above,
// but also b) skip the search unless we have at least 3
// characters of input from user
if (entry.length < 3) {
this.data = []
return
}
// and perform the search
let url = '${url(f'{route_prefix}.product_autocomplete')}'
this.$http.get(url + '?term=' + encodeURIComponent(entry))
.then(({ data }) => {
this.autocompleteOptions = data
}).catch((error) => {
this.autocompleteOptions = []
throw error
})
}),
autocompleteSelected(option) {
this.$emit('selected', {
uuid: option.value,
full_description: option.label,
})
},
lookupInit() {
this.searchResultSelected = null this.searchResultSelected = null
this.lookupShowDialog = true
if (term !== undefined) { this.$nextTick(() => {
this.searchTerm = term
// perform search if invoked with new term this.searchTerm = this.autocompleteValue
if (term != this.searchTermLastUsed) { if (this.searchTerm != this.searchTermLastUsed) {
this.searchTermLastUsed = null this.searchTermLastUsed = null
this.performSearch() this.performSearch()
} }
} else {
this.searchTerm = this.searchTermLastUsed
}
this.showingDialog = true
this.$nextTick(() => {
this.$refs.searchTermInput.focus() this.$refs.searchTermInput.focus()
}) })
}, },
@ -214,17 +308,6 @@
} }
}, },
cancelDialog() {
this.searchResultSelected = null
this.showingDialog = false
this.$emit('canceled')
},
selectResult() {
this.showingDialog = false
this.$emit('selected', this.searchResultSelected)
},
performSearch() { performSearch() {
if (this.searchResultsLoading) { if (this.searchResultsLoading) {
return return
@ -255,6 +338,16 @@
this.searchResultsLoading = false this.searchResultsLoading = false
}) })
}, },
selectResult() {
this.lookupShowDialog = false
this.$emit('selected', this.searchResultSelected)
},
cancelDialog() {
this.searchResultSelected = null
this.lookupShowDialog = false
},
}, },
} }