Fix the levels viewer: markers were drawn on whichever plan was showing

This commit is contained in:
2026-08-17 15:01:09 -04:00
parent ece0e15f03
commit 4c3d5746e8
11 changed files with 214 additions and 38 deletions

View File

@@ -10,6 +10,17 @@ ADR-007 and ADR-002.
## [Unreleased] ## [Unreleased]
## [0.11.1] - 2026-08-17
A patch release: the pagination-cap fixes, plus the documentation that missed the
0.11.0 installer. No application behaviour changed on the server and the plugin
contract stays at 0.20.0.
`docs/UPGRADE.md` is one of the documents the Windows installer puts on the box,
and its buildings-and-levels section landed after 0.11.0 was tagged - so 0.11.0
shipped the page an operator reads WHILE upgrading without the guidance about
that upgrade. An air-gapped server has no other way to reach it.
### Fixed ### Fixed
- Pickers and batch label sheets no longer stop at 100 rows. `perpage` is - Pickers and batch label sheets no longer stop at 100 rows. `perpage` is
@@ -25,6 +36,16 @@ ADR-007 and ADR-002.
`PrintedPartsLabels`, `MachineForm` and `NetworkDeviceForm`. Lists that render `PrintedPartsLabels`, `MachineForm` and `NetworkDeviceForm`. Lists that render
a page at a time are unchanged - they page deliberately. a page at a time are unchanged - they page deliberately.
### Changed
- The installer now also ships `FLOOR-MAP.md`, the operator's page for loading
floor plans, placing markers and moving them when a plan changes. Same reason
the Windows runbooks ship: the box cannot reach the wiki.
- The bundled `UPGRADE.md` now carries the 0.11.0 section: an existing map is
preserved and nothing moves on screen, and replacing a blueprint with one of
different dimensions moves every marker on that level, so recalibrate from
landmarks rather than editing the level's width and height.
## [0.11.0] - 2026-08-17 ## [0.11.0] - 2026-08-17
The floor map became a set of drawings instead of one picture. A site can hold The floor map became a set of drawings instead of one picture. A site can hold

View File

@@ -1,6 +1,6 @@
{ {
"name": "shopdb-frontend", "name": "shopdb-frontend",
"version": "0.11.0", "version": "0.11.1",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@@ -33,6 +33,10 @@
--sidebar-bg: #00003d; --sidebar-bg: #00003d;
--sidebar-text: #ffffff; --sidebar-text: #ffffff;
--sidebar-width: 250px; --sidebar-width: 250px;
/* Vertical chrome around .main-content. A full-height page subtracts these;
hardcoding a different number is what put a scrollbar on the map. */
--main-pad-top: 20px;
--main-pad-bottom: 70px;
/* Hover variants */ /* Hover variants */
--secondary-dark: #82503f; --secondary-dark: #82503f;
@@ -243,7 +247,7 @@ h1, h2, h3, h4, h5, h6 {
.main-content { .main-content {
flex: 1; flex: 1;
margin-left: var(--sidebar-width); margin-left: var(--sidebar-width);
padding: 20px 10px 70px 10px; padding: var(--main-pad-top) 10px var(--main-pad-bottom) 10px;
overflow-x: hidden; overflow-x: hidden;
} }

View File

@@ -437,8 +437,18 @@ function renderMarkers() {
overlayLayers.forEach(layer => layer.remove()) overlayLayers.forEach(layer => layer.remove())
overlayLayers = [] overlayLayers = []
// Markers belong to ONE level (ADR-017). This is the drawing being shown, and
// anything positioned against a different one is not drawn on it.
const drawnLevel = drawnLevelId() ?? null
props.machines.forEach(item => { props.machines.forEach(item => {
if (item.mapx == null || item.mapy == null) return if (item.mapx == null || item.mapy == null) return
// A marker from another level, placed on THIS blueprint, looks entirely
// correct and points at the wrong part of the building - so it is omitted
// rather than approximated. A position with no level at all is omitted for
// the same reason: the map editor lists both, badged, so they can be fixed
// rather than silently misplaced here.
if ((item.levelid ?? null) !== drawnLevel) return
// Transform coordinates (database Y is top-down, Leaflet is bottom-up) // Transform coordinates (database Y is top-down, Leaflet is bottom-up)
const leafletY = MAP_HEIGHT - item.mapy const leafletY = MAP_HEIGHT - item.mapy
@@ -599,6 +609,31 @@ watch(() => props.theme, (newTheme) => {
} }
}) })
// Switching level changes the drawing, the coordinate space AND which markers
// belong on it. All three move together: the size is what marker coordinates
// mean, so swapping the image without the bounds would place every marker
// against the wrong scale, and keeping the markers would show the previous
// floor's assets on this floor's plan.
watch(() => drawnLevelId(), (levelid) => {
if (!map || !imageOverlay) return
MAP_WIDTH = dimensionsFor(levelid).width
MAP_HEIGHT = dimensionsFor(levelid).height
const bounds = [[0, 0], [MAP_HEIGHT, MAP_WIDTH]]
const url = blueprintUrlFor(props.theme, levelid)
// A level with no blueprint is left blank rather than showing the previous
// one, which would be a floor plan labelled as somewhere it is not.
imageOverlay.setUrl(url || '')
imageOverlay.setBounds(bounds)
map.setMaxBounds(bounds)
map.setView([MAP_HEIGHT / 2, MAP_WIDTH / 2], map.getZoom())
renderMarkers()
loadOverlays()
})
onMounted(async () => { onMounted(async () => {
// Load this facility's blueprint + dimensions before building the map so // Load this facility's blueprint + dimensions before building the map so
// bounds and coordinate math use the right size. Falls back to defaults. // bounds and coordinate math use the right size. Falls back to defaults.

View File

@@ -492,7 +492,7 @@ function cancelEdit() {
.map-editor { .map-editor {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: calc(100vh - 40px); height: calc(100vh - var(--main-pad-top) - var(--main-pad-bottom));
} }
.header-actions { .header-actions {

View File

@@ -12,6 +12,17 @@
<template v-else> <template v-else>
<!-- Filter Controls --> <!-- Filter Controls -->
<div class="map-filters"> <div class="map-filters">
<select
v-if="levelChoices.length > 1"
v-model.number="shownLevelId"
@change="onLevelChange"
title="Which floor plan to show"
>
<option v-for="level in levelChoices" :key="level.levelid" :value="level.levelid">
{{ level.label }}
</option>
</select>
<select v-model="selectedType" @change="onTypeChange"> <select v-model="selectedType" @change="onTypeChange">
<option value="">All Asset Types</option> <option value="">All Asset Types</option>
<option v-for="t in assetTypes" :key="t.assettypeid" :value="t.assettype"> <option v-for="t in assetTypes" :key="t.assettypeid" :value="t.assettype">
@@ -60,6 +71,7 @@
</div> </div>
<ShopFloorMap <ShopFloorMap
:levelid="shownLevelId"
:machines="filteredAssets" :machines="filteredAssets"
:machinetypes="[]" :machinetypes="[]"
:businessunits="businessunits" :businessunits="businessunits"
@@ -82,7 +94,8 @@ import ShopFloorMap from '../components/ShopFloorMap.vue'
import { assetsApi } from '../api' import { assetsApi } from '../api'
import { currentTheme } from '../stores/theme' import { currentTheme } from '../stores/theme'
import { useAuthStore } from '../stores/auth' import { useAuthStore } from '../stores/auth'
import { loadMapConfig, blueprintUrlFor, dimensionsFor, levelName, state as mapConfig } from '@/composables/mapConfig' import { loadMapConfig, blueprintUrlFor, dimensionsFor, levelName, levelOptions,
setCurrentLevel, state as mapConfig } from '@/composables/mapConfig'
import { exportMapPdf } from '../utils/mapPdf' import { exportMapPdf } from '../utils/mapPdf'
import { assetTypeLabel, assetDetailRoute } from '../utils/assetTypes' import { assetTypeLabel, assetDetailRoute } from '../utils/assetTypes'
import { getSubtypeId } from '../utils/mapColors' import { getSubtypeId } from '../utils/mapColors'
@@ -104,6 +117,41 @@ const selectedSubtype = ref('')
const selectedBusinessUnit = ref('') const selectedBusinessUnit = ref('')
const selectedStatus = ref('') const selectedStatus = ref('')
const searchQuery = ref('') const searchQuery = ref('')
// Which floor plan is on screen. Every marker coordinate is pixels of ONE level
// (ADR-017), so this decides both the drawing and which markers belong on it.
const shownLevelId = ref(null)
const levelChoices = computed(() => levelOptions())
function onLevelChange() {
// Keep the shared composable in step, so a map-position picker opened later
// starts on the level being looked at rather than the site default.
setCurrentLevel(shownLevelId.value)
}
// A search whose only matches are on another floor would otherwise show an empty
// map: the assets matched, the filter counted them, and nothing was drawn.
// Follow the results to the level that actually holds them - the level with the
// most matches, so a search matching several floors lands on the best one.
function followSearchAcrossLevels() {
if (!searchQuery.value.trim()) return
const matches = filteredAssets.value.filter(a => a.mapx != null && a.mapy != null)
if (!matches.length) return
if (matches.some(a => (a.levelid ?? null) === shownLevelId.value)) return
const tally = new Map()
matches.forEach(asset => {
const levelid = asset.levelid ?? null
if (levelid === null) return
tally.set(levelid, (tally.get(levelid) || 0) + 1)
})
if (!tally.size) return
const best = [...tally.entries()].sort((a, b) => b[1] - a[1])[0][0]
shownLevelId.value = best
setCurrentLevel(best)
}
const exporting = ref(false) const exporting = ref(false)
let searchTimeout = null let searchTimeout = null
@@ -245,12 +293,12 @@ async function exportPdf() {
// blueprintUrlFor applies withBase - the raw setting value is a // blueprintUrlFor applies withBase - the raw setting value is a
// root-relative /api path, which 404s under a subpath mount like /ops. // root-relative /api path, which 404s under a subpath mount like /ops.
// The PDF always uses the light blueprint: it prints on white paper. // The PDF always uses the light blueprint: it prints on white paper.
blueprintUrl: blueprintUrlFor('light', mapConfig.currentlevelid), blueprintUrl: blueprintUrlFor('light', shownLevelId.value),
mapWidth: dimensionsFor(mapConfig.currentlevelid).width, mapWidth: dimensionsFor(shownLevelId.value).width,
mapHeight: dimensionsFor(mapConfig.currentlevelid).height, mapHeight: dimensionsFor(shownLevelId.value).height,
// Named on the sheet, because a floor plan with no level on it is not // Named on the sheet, because a floor plan with no level on it is not
// identifiable once it is printed and carried to the floor. // identifiable once it is printed and carried to the floor.
levelname: levelName(mapConfig.currentlevelid), levelname: levelName(shownLevelId.value),
selectedType: selectedType.value, selectedType: selectedType.value,
subtypeColors: subtypeColorMap.value, subtypeColors: subtypeColorMap.value,
subtypeNames: subtypeNameMap.value, subtypeNames: subtypeNameMap.value,
@@ -265,7 +313,8 @@ async function exportPdf() {
} }
onMounted(async () => { onMounted(async () => {
loadMapConfig() await loadMapConfig()
shownLevelId.value = mapConfig.currentlevelid ?? mapConfig.defaultlevelid ?? null
try { try {
const response = await assetsApi.getMap() const response = await assetsApi.getMap()
const data = response.data.data || {} const data = response.data.data || {}
@@ -305,6 +354,7 @@ function updateMapLayers() {
function debouncedSearch() { function debouncedSearch() {
clearTimeout(searchTimeout) clearTimeout(searchTimeout)
searchTimeout = setTimeout(() => { searchTimeout = setTimeout(() => {
followSearchAcrossLevels()
updateMapLayers() updateMapLayers()
}, 300) }, 300)
} }
@@ -318,7 +368,7 @@ function handleMarkerClick(asset) {
.map-page { .map-page {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: calc(100vh - 2rem); height: calc(100vh - var(--main-pad-top) - var(--main-pad-bottom));
} }
.map-page .page-header { .map-page .page-header {

View File

@@ -213,9 +213,18 @@ async function search(q) {
} }
function performSearch() { function performSearch() {
if (searchInput.value.trim()) { const term = searchInput.value.trim()
router.push({ path: '/search', query: { q: searchInput.value.trim() } }) if (!term) return
// Searching a term that is ALREADY in the URL is a duplicate navigation: the
// router aborts it, so the route watcher never fires and the button does
// nothing at all. Re-run the query directly in that case. The sidebar box
// never hits this, because it is always navigating from somewhere else -
// which is why the two search boxes appeared to behave differently.
if (term === route.query.q) {
search(term)
return
} }
router.push({ path: '/search', query: { q: term } })
} }
async function openKBArticle(result) { async function openKBArticle(result) {

View File

@@ -62,18 +62,36 @@
/> />
</td> </td>
<td> <td>
<span class="mono">{{ level.mapwidth }} x {{ level.mapheight }}</span> <div class="size-fields">
<input
v-model.number="level.mapwidth"
type="number"
min="1"
class="form-control size-input"
aria-label="Blueprint width in pixels"
@blur="saveSize(level)"
:disabled="saving"
/>
<span class="size-x">x</span>
<input
v-model.number="level.mapheight"
type="number"
min="1"
class="form-control size-input"
aria-label="Blueprint height in pixels"
@blur="saveSize(level)"
:disabled="saving"
/>
</div>
<small v-if="level.assetcount" class="input-hint"> <small v-if="level.assetcount" class="input-hint">
fixed while {{ level.assetcount }} marker(s) are placed Taken from the image while a level is empty. Changing it now
re-scales where all {{ level.assetcount }} marker(s) sit relative
to the drawing - set the new size, then Recalibrate from
landmarks in the map editor.
</small>
<small v-else class="input-hint">
Set from the blueprint you upload.
</small> </small>
<input
v-else
v-model.number="level.mapwidth"
type="number"
class="form-control size-input"
@blur="saveLevel(level, { mapwidth: level.mapwidth, mapheight: level.mapheight })"
:disabled="saving"
/>
</td> </td>
<td> <td>
<span :class="{ 'muted': !level.assetcount }">{{ level.assetcount }}</span> <span :class="{ 'muted': !level.assetcount }">{{ level.assetcount }}</span>
@@ -311,6 +329,29 @@ async function saveLevel(level, payload) {
} }
} }
async function saveSize(level) {
if (!(level.mapwidth > 0) || !(level.mapheight > 0)) {
error.value = 'Width and height must both be positive.'
await load()
return
}
// A level with markers keeps its coordinates when the size changes, so every
// marker moves relative to the drawing. That is sometimes exactly right (a
// re-export of the same plan at a new resolution) and sometimes the start of
// a recalibration, but it is never something to do by accident.
if (level.assetcount) {
const ok = window.confirm(
`${level.levelname} has ${level.assetcount} marker(s) placed against ` +
`${level.mapwidth} x ${level.mapheight}. Changing the size moves all of ` +
`them relative to the drawing. Recalibrate from landmarks afterwards. Continue?`)
if (!ok) {
await load()
return
}
}
await saveLevel(level, { mapwidth: level.mapwidth, mapheight: level.mapheight })
}
async function upload(level, theme, event) { async function upload(level, theme, event) {
const file = event.target.files?.[0] const file = event.target.files?.[0]
if (!file) return if (!file) return
@@ -398,6 +439,8 @@ async function remove(level) {
.mono { font-family: monospace; } .mono { font-family: monospace; }
.order-input { width: 4.5rem; } .order-input { width: 4.5rem; }
.size-input { width: 6rem; } .size-input { width: 6rem; }
.size-fields { display: flex; align-items: center; gap: 0.35rem; }
.size-x { color: var(--text-light); }
.file-input { display: block; margin-top: 0.25rem; font-size: 0.75rem; } .file-input { display: block; margin-top: 0.25rem; font-size: 0.75rem; }
.map-thumb { .map-thumb {

View File

@@ -95,7 +95,8 @@ python3 "$REPO/scripts/generate_sbom.py" "$REPO" -o "$OUT/sbom.cdx.json"
echo "==> Staging docs ..." echo "==> Staging docs ..."
mkdir -p "$OUT/docs" mkdir -p "$OUT/docs"
for doc in openapi.json llms.txt api-inventory.json \ for doc in openapi.json llms.txt api-inventory.json \
INSTALL-WINDOWS.md OPERATE-WINDOWS.md BACKUP-RESTORE.md UPGRADE.md; do INSTALL-WINDOWS.md OPERATE-WINDOWS.md BACKUP-RESTORE.md UPGRADE.md \
FLOOR-MAP.md; do
[ -f "$REPO/docs/$doc" ] && cp -a "$REPO/docs/$doc" "$OUT/docs/" [ -f "$REPO/docs/$doc" ] && cp -a "$REPO/docs/$doc" "$OUT/docs/"
done done
# Never let a missing optional doc fail the build - the loop's last test governs # Never let a missing optional doc fail the build - the loop's last test governs

View File

@@ -63,7 +63,7 @@ __contract_version__ = '0.20.0'
# plugin-contract version above are distinct series with independent # plugin-contract version above are distinct series with independent
# bump rules. Not part of the shopdb.api contract surface, so it is # bump rules. Not part of the shopdb.api contract surface, so it is
# not re-exported there. # not re-exported there.
__version__ = '0.11.0' __version__ = '0.11.1'
def create_app(config_name: str = None) -> Flask: def create_app(config_name: str = None) -> Flask:

View File

@@ -23,6 +23,14 @@ logger = logging.getLogger(__name__)
search_bp = Blueprint('search', __name__) search_bp = Blueprint('search', __name__)
# EVERY searcher below truncates with .limit(). An unordered LIMIT lets the
# database return a DIFFERENT subset of the matching rows between two identical
# requests - the same search run twice came back with different hits, which reads
# as the search being broken rather than as a missing ORDER BY. Each query
# therefore ends with a TOTAL order: a display key plus the primary key, so ties
# cannot reorder. Relevance is applied afterwards in Python, over a stable set.
def _word_match(query, *columns): def _word_match(query, *columns):
"""SQL clause matching rows that contain EVERY word of the query, each word """SQL clause matching rows that contain EVERY word of the query, each word
in any of the columns, in any order. in any of the columns, in any order.
@@ -213,7 +221,7 @@ def _search_applications(query, search_term):
apps = Application.query.filter( apps = Application.query.filter(
Application.isactive == True, Application.isactive == True,
_word_match(query, Application.appname, Application.appdescription) _word_match(query, Application.appname, Application.appdescription)
).limit(10).all() ).order_by(Application.appname, Application.appid).limit(10).all()
for app in apps: for app in apps:
relevance = 20 relevance = 20
@@ -258,7 +266,8 @@ def _search_knowledgebase(query, search_term):
KnowledgeBase.appid.notin_(retired)), KnowledgeBase.appid.notin_(retired)),
_word_match(query, KnowledgeBase.shortdescription, _word_match(query, KnowledgeBase.shortdescription,
KnowledgeBase.keywords) KnowledgeBase.keywords)
).limit(20).all() ).order_by(KnowledgeBase.clicks.desc(),
KnowledgeBase.linkid).limit(20).all()
for kb in kb_articles: for kb in kb_articles:
relevance = 10 + (kb.clicks or 0) * 0.1 relevance = 10 + (kb.clicks or 0) * 0.1
@@ -378,7 +387,7 @@ def _search_assets(query, search_term):
_word_match(query, Asset.assetnumber, Asset.name, _word_match(query, Asset.assetnumber, Asset.name,
Asset.serialnumber, Asset.notes, Asset.serialnumber, Asset.notes,
Asset.gaugelabreference, Asset.maintenancereference) Asset.gaugelabreference, Asset.maintenancereference)
).limit(15).all() ).order_by(Asset.assetnumber, Asset.assetid).limit(15).all()
for asset in assets: for asset in assets:
relevance = 15 relevance = 15
@@ -421,7 +430,7 @@ def _search_measuringtools(query, search_term):
Asset.isactive == True, Asset.isactive == True,
_word_match(query, Asset.assetnumber, Asset.name, _word_match(query, Asset.assetnumber, Asset.name,
Asset.serialnumber, Asset.gaugelabreference) Asset.serialnumber, Asset.gaugelabreference)
).limit(15).all() ).order_by(Asset.assetnumber, Asset.assetid).limit(15).all()
for asset in assets: for asset in assets:
relevance = 15 relevance = 15
@@ -461,7 +470,8 @@ def _search_usbdevices(query, search_term):
USBDevice.isactive == True, USBDevice.isactive == True,
_word_match(query, USBDevice.serialnumber, USBDevice.assetnumber, _word_match(query, USBDevice.serialnumber, USBDevice.assetnumber,
USBDevice.label, USBDevice.productname) USBDevice.label, USBDevice.productname)
).limit(10).all() ).order_by(USBDevice.serialnumber,
USBDevice.usbdeviceid).limit(10).all()
for device in devices: for device in devices:
relevance = 20 relevance = 20
@@ -507,7 +517,8 @@ def _search_printeditems(query, search_term):
PrintedItem.isactive == True, PrintedItem.isactive == True,
_word_match(query, PrintedItem.itemcode, PrintedItem.gagelabtag, _word_match(query, PrintedItem.itemcode, PrintedItem.gagelabtag,
PrintedItem.itemname, PrintedItem.itemdescription) PrintedItem.itemname, PrintedItem.itemdescription)
).limit(10).all() ).order_by(PrintedItem.itemcode,
PrintedItem.printeditemid).limit(10).all()
for item in items: for item in items:
relevance = 20 relevance = 20
@@ -563,7 +574,7 @@ def _search_customfields(query, search_term):
CustomField.searchable == True, CustomField.searchable == True,
CustomField.isactive == True, CustomField.isactive == True,
_word_match(query, CustomFieldValue.value), _word_match(query, CustomFieldValue.value),
).limit(15).all() ).order_by(Asset.assetnumber, CustomField.fieldid).limit(15).all()
for asset, field in rows: for asset, field in rows:
result = _get_asset_result(asset, query, relevance=40) result = _get_asset_result(asset, query, relevance=40)
@@ -583,7 +594,8 @@ def _search_by_ip(query, search_term):
).options( ).options(
joinedload(Communication.asset).joinedload(Asset.assettype), joinedload(Communication.asset).joinedload(Asset.assettype),
joinedload(Communication.asset).joinedload(Asset.location), joinedload(Communication.asset).joinedload(Asset.location),
).limit(10).all() ).order_by(Communication.ipaddress,
Communication.communicationid).limit(10).all()
seen_assets = set() seen_assets = set()
for comm in comms: for comm in comms:
@@ -643,7 +655,7 @@ def _search_hostnames(query, search_term):
).options( ).options(
joinedload(Computer.asset).joinedload(Asset.assettype), joinedload(Computer.asset).joinedload(Asset.assettype),
joinedload(Computer.asset).joinedload(Asset.location), joinedload(Computer.asset).joinedload(Asset.location),
).limit(10).all() ).order_by(Computer.hostname, Computer.computerid).limit(10).all()
for comp in computers: for comp in computers:
if comp.asset and comp.asset.isactive: if comp.asset and comp.asset.isactive:
@@ -666,7 +678,7 @@ def _search_hostnames(query, search_term):
).options( ).options(
joinedload(Printer.asset).joinedload(Asset.assettype), joinedload(Printer.asset).joinedload(Asset.assettype),
joinedload(Printer.asset).joinedload(Asset.location), joinedload(Printer.asset).joinedload(Asset.location),
).limit(10).all() ).order_by(Printer.hostname, Printer.printerid).limit(10).all()
for printer in printers: for printer in printers:
if printer.asset and printer.asset.isactive: if printer.asset and printer.asset.isactive:
@@ -689,7 +701,8 @@ def _search_hostnames(query, search_term):
).options( ).options(
joinedload(NetworkDevice.asset).joinedload(Asset.assettype), joinedload(NetworkDevice.asset).joinedload(Asset.assettype),
joinedload(NetworkDevice.asset).joinedload(Asset.location), joinedload(NetworkDevice.asset).joinedload(Asset.location),
).limit(10).all() ).order_by(NetworkDevice.hostname,
NetworkDevice.networkdeviceid).limit(10).all()
for device in devices: for device in devices:
if device.asset and device.asset.isactive: if device.asset and device.asset.isactive:
@@ -773,7 +786,7 @@ def _search_vendor_model_type(query, search_term):
Asset.isactive == True, Asset.isactive == True,
_word_match(query, Vendor.vendor, Model.modelnumber, _word_match(query, Vendor.vendor, Model.modelnumber,
MachineType.machinetype) MachineType.machinetype)
).limit(10).all() ).order_by(Asset.assetnumber, Asset.assetid).limit(10).all()
for asset in machine_assets: for asset in machine_assets:
results.append(_get_asset_result(asset, query, 30)) results.append(_get_asset_result(asset, query, 30))
@@ -801,7 +814,7 @@ def _search_vendor_model_type(query, search_term):
Asset.isactive == True, Asset.isactive == True,
_word_match(query, Vendor.vendor, Model.modelnumber, _word_match(query, Vendor.vendor, Model.modelnumber,
PrinterType.printertype) PrinterType.printertype)
).limit(10).all() ).order_by(Asset.assetnumber, Asset.assetid).limit(10).all()
for asset in printer_assets: for asset in printer_assets:
results.append(_get_asset_result(asset, query, 30)) results.append(_get_asset_result(asset, query, 30))
@@ -827,7 +840,7 @@ def _search_vendor_model_type(query, search_term):
Asset.isactive == True, Asset.isactive == True,
_word_match(query, Vendor.vendor, _word_match(query, Vendor.vendor,
NetworkDeviceType.networkdevicetype) NetworkDeviceType.networkdevicetype)
).limit(10).all() ).order_by(Asset.assetnumber, Asset.assetid).limit(10).all()
for asset in netdev_assets: for asset in netdev_assets:
results.append(_get_asset_result(asset, query, 30)) results.append(_get_asset_result(asset, query, 30))