4 Commits

Author SHA1 Message Date
cproudlock
8bde89c47e Release 0.11.2
Some checks failed
CI / backend (push) Failing after 7s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 8s
CI / migrations-mysql (push) Failing after 6s
Bug fixes for the buildings-and-levels work in 0.11.0, every one of them found by
using the feature rather than by the suite.

Anyone running 0.11.0 or 0.11.1 with more than one level should take this
release: the map drew markers from every level on whichever floor plan happened
to be showing, which is the failure ADR-017 exists to prevent, and it was in the
one component that draws the map. The viewer also had no way to choose a level,
and the editor never accepted a click at all - its handler was bound only if the
map was already a picker at mount, and the editor opens with nothing selected.

Also: a level's native size could not be set (the settings page had no height
field), the same search could return different rows because fourteen searchers
truncated without an ORDER BY, and both map pages carried a scrollbar from
subtracting the wrong page chrome from the viewport height.

No schema change, and the plugin contract stays at 0.20.0.

The version and the changelog are the release; the detail is in the entry.
2026-08-17 15:46:38 -04:00
cproudlock
afd3dce493 Give the map editor's search box room for its own placeholder
Some checks failed
CI / backend (push) Failing after 7s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 9s
CI / migrations-mysql (push) Failing after 7s
The previous commit stopped the panel header from colliding with the "Assets"
heading, but left the search box sharing a row with the type dropdown: 139px of
input for a placeholder needing 216px, so it still read "Search name o". A
control whose own label does not fit is not a narrower control, it is an
unlabelled one.

One control per row in a 318px panel. Measured in the browser: the input is now
286px against 216px of text, so the placeholder reads in full.
2026-08-17 15:24:37 -04:00
cproudlock
89248407e7 Make the map editor accept a click, and unclutter its panel
Some checks failed
CI / backend (push) Failing after 8s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 9s
CI / migrations-mysql (push) Failing after 7s
Clicking the map in the editor did nothing, ever. The click handler was bound
inside `if (props.pickerMode)` at mount, and the editor mounts with no asset
selected, so the handler was never attached; selecting an asset flipped the prop
but nothing rebound it. The asset forms were unaffected because their picker
mounts inside a modal that is already in picker mode, which is why this looked
like an editor-only fault. The handler is now bound unconditionally and
handleMapClick keeps its own picker-mode guard.

Verified in a browser against the dev instance, both ways: with the old binding
a click on a selected asset produced no position at all; with the fix the same
click reports 1652, 1138.

Markers are now drawn in picker mode too. Placing one relative to the machines
already on the floor is the entire task, and the old code skipped rendering them
whenever the map was a picker.

Two things that looked like stray widgets:
- The editor's panel header put a heading and three controls on one row inside a
  320px panel, squeezing the search box until its placeholder read "Search na".
  The heading takes its own row and the controls share the next.
- The legend drew its bar and border even with nothing to put in it, which read
  as an empty input box under the toolbar. It renders only when it has entries.
2026-08-17 15:20:44 -04:00
cproudlock
aa6db94179 Fix the levels viewer: markers were drawn on whichever plan was showing
Some checks failed
CI / backend (push) Failing after 8s
CI / naming (push) Successful in 3s
CI / frontend (push) Successful in 9s
CI / migrations-mysql (push) Failing after 8s
Five defects, four of them mine from 0.11.0, found by using the feature.

THE SERIOUS ONE: ShopFloorMap never checked a marker's level. It skipped null
coordinates and drew everything else on whatever blueprint was displayed, so
level 1 markers appeared on level 2 - the exact failure ADR-017 exists to
prevent, in the one component that draws the map. The build gate did not catch it
because that rule checks files EMITTING mapx, not the component consuming it.
Markers are now filtered to the drawn level, and a position with no level is
omitted rather than approximated.

The map page had no way to choose a level at all. It read currentlevelid only to
title the PDF, so a second floor was unreachable from the viewer that most people
use. Adds a level selector (hidden when a site has one level), passes it to the
map, and switches the drawing, the bounds, the coordinate space and the markers
together - swapping the image without the bounds would place every marker against
the wrong scale.

Searching the map now follows results across levels: a search whose matches are
all on another floor showed an empty map while the filter counted them.

Floor map settings had NO height input - only width - so a level's native size
could not be set even while empty, which is the one time it is editable. Both
fields are there now, and size is editable on a level that has markers, because
refusing it blocked the case the feature was built for: a new blueprint of new
dimensions on a floor already full of markers. It confirms first and points at
landmark recalibration.

Search results differed between the sidebar box and the results-page box:
- 14 of 16 searchers truncated with .limit() and no ORDER BY, so the database
  could return a DIFFERENT subset of matching rows for the same query. Every
  searcher now ends in a total order (display key plus primary key).
- Searching a term already in the URL was a duplicate navigation the router
  aborts, so the route watcher never fired and the button did nothing. The
  sidebar never hit this, because it always navigates from another page - which
  is why the two boxes appeared to disagree.

Also removes a scrollbar from both map pages. They subtracted 2rem and 40px from
100vh for the page chrome, which is really 90px of padding on .main-content, so
each overflowed by the difference. The padding is now a CSS variable both the
layout and the pages read. Measured in the browser before and after: 1058 vs a
1000px viewport, now 1000.
2026-08-17 14:59:44 -04:00
11 changed files with 282 additions and 49 deletions

View File

@@ -10,6 +10,55 @@ ADR-007 and ADR-002.
## [Unreleased]
## [0.11.2] - 2026-08-17
Bug fixes for the buildings-and-levels work in 0.11.0, all found by using it.
Anyone on 0.11.0 or 0.11.1 with more than one level should take this: until now
the map drew markers from every level on whichever floor plan was showing.
### Fixed
- **Markers from other levels were drawn on the displayed level.** The map
component filtered out positions with no coordinates and drew everything else,
never checking which drawing a position belonged to - the exact failure ADR-017
exists to prevent, in the one component that draws the map. Markers are now
filtered to the level being shown, and a position with no level is omitted
rather than approximated onto the default.
- **The map page had no way to choose a level.** It read the current level only
to title a PDF export, so a second floor was unreachable from the viewer most
people use. A level selector appears when a site has more than one, and
switching moves the drawing, the bounds, the coordinate space and the markers
together.
- **The map editor never accepted a click.** Its click handler was bound only if
the map was already in picker mode when it mounted, and the editor opens with
nothing selected, so selecting an asset and clicking did nothing at all for the
life of the page. The asset forms were unaffected, because their picker is
mounted inside a dialog that is already in picker mode.
- Existing markers are now drawn while placing one. Positioning an asset relative
to the machines already on the floor is the whole task.
- Searching the map follows results across levels: a search whose matches are all
on another floor showed an empty map while the filter counted them.
- **A level's native size could not be set.** The settings page had a width field
and no height field at all, and locked both once any marker was placed - which
blocked the case the feature was built for, a new blueprint of new dimensions
on a floor that already has markers. Both fields are editable, with a
confirmation and a pointer to landmark recalibration.
- **The same search could return different results.** Fourteen of the sixteen
searchers behind global search truncated with `LIMIT` and no `ORDER BY`, so the
database was free to return a different subset of matching rows each time.
Every searcher now ends in a total order.
- Re-searching a term already in the address bar did nothing: the router treats
it as a duplicate navigation and aborts, so the results page never re-queried.
This is why the sidebar box and the results-page box appeared to disagree.
- The empty legend no longer draws its bar and border, which read as a stray
input box under the map toolbar.
- The map editor's panel header no longer crams a heading and three controls onto
one row, which had squeezed the search box until its placeholder read
"Search na".
- Both map pages had a scrollbar. They subtracted the wrong amount from the
viewport height for the page chrome, so each overflowed by the difference; the
padding is now a value the pages and the layout share.
## [0.11.1] - 2026-08-17
A patch release: the pagination-cap fixes, plus the documentation that missed the

View File

@@ -11,7 +11,7 @@ never by editing this file.
| series | value | governed by |
|---|---|---|
| product `__version__` | `0.11.1` | ADR-007 |
| product `__version__` | `0.11.2` | ADR-007 |
| plugin contract `__contract_version__` | `0.20.0` | ADR-002 |
They move independently. A contract bump is not a release.

View File

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

View File

@@ -33,6 +33,10 @@
--sidebar-bg: #00003d;
--sidebar-text: #ffffff;
--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 */
--secondary-dark: #82503f;
@@ -243,7 +247,7 @@ h1, h2, h3, h4, h5, h6 {
.main-content {
flex: 1;
margin-left: var(--sidebar-width);
padding: 20px 10px 70px 10px;
padding: var(--main-pad-top) 10px var(--main-pad-bottom) 10px;
overflow-x: hidden;
}

View File

@@ -45,7 +45,7 @@
</div>
<!-- Legend for asset type mode - shows subtypes when a type is selected -->
<div class="map-legend" v-if="!pickerMode && assetTypeMode">
<div class="map-legend" v-if="!pickerMode && assetTypeMode && hasLegendEntries">
<!-- Show subtype legend when a specific type is selected -->
<template v-if="selectedAssetType && Object.keys(visibleSubtypes).length">
<span
@@ -120,6 +120,14 @@ const props = defineProps({
const emit = defineEmits(['markerClick', 'positionPicked'])
// An empty legend still drew its bar and border, which looked like a stray input
// box under the toolbar. It renders only when it has something in it.
const hasLegendEntries = computed(() => {
if (props.selectedAssetType && Object.keys(visibleSubtypes.value).length) return true
if (Object.keys(visibleAssetTypes.value).length) return true
return overlayLegend.value.length > 0
})
const mapContainer = ref(null)
let map = null
let imageOverlay = null
@@ -296,17 +304,22 @@ function initMap() {
map.setView([MAP_HEIGHT / 2, MAP_WIDTH / 2], initialZoom)
map.setMaxBounds(bounds)
// Picker mode: click to set position
if (props.pickerMode) {
map.on('click', handleMapClick)
// Bound UNCONDITIONALLY, and handleMapClick ignores the click when picker mode
// is off. Binding it only when pickerMode was true AT MOUNT meant a map that
// becomes a picker later never got a click handler at all: the map editor
// opens with nothing selected, so clicking to place a marker did nothing, for
// the whole life of the page. The asset forms only worked because their picker
// is mounted inside a modal that is already in picker mode.
map.on('click', handleMapClick)
// Show initial position if provided
if (props.initialPosition) {
setPickerPosition(props.initialPosition.left, props.initialPosition.top)
}
} else {
renderMarkers()
if (props.pickerMode && props.initialPosition) {
setPickerPosition(props.initialPosition.left, props.initialPosition.top)
}
// Existing markers are drawn in picker mode too: placing a marker relative to
// the ones already on the floor is the whole task. The forms pass no markers,
// so this costs them nothing.
renderMarkers()
}
function handleMapClick(e) {
@@ -437,8 +450,18 @@ function renderMarkers() {
overlayLayers.forEach(layer => layer.remove())
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 => {
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)
const leafletY = MAP_HEIGHT - item.mapy
@@ -599,6 +622,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 () => {
// Load this facility's blueprint + dimensions before building the map so
// bounds and coordinate math use the right size. Falls back to defaults.

View File

@@ -492,7 +492,7 @@ function cancelEdit() {
.map-editor {
display: flex;
flex-direction: column;
height: calc(100vh - 40px);
height: calc(100vh - var(--main-pad-top) - var(--main-pad-bottom));
}
.header-actions {
@@ -518,13 +518,30 @@ function cancelEdit() {
}
.panel-header {
/* The heading takes its own row and the three controls share the next one(s).
All four on a single row inside a 320px panel squeezed the search box until
its placeholder read "Search na". */
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
padding: 1rem;
border-bottom: 1px solid var(--border);
}
.panel-header h3 {
flex: 1 1 100%;
}
.panel-header .form-control {
/* One control per row. Two of them side by side in a 318px panel left the
search box 139px for a placeholder needing 216px, so it read "Search name
o" - a control whose own label does not fit is not a narrower control, it is
an unlabelled one. */
flex: 1 1 100%;
min-width: 0;
}
.panel-header h3 {
margin: 0;
font-size: 1rem;

View File

@@ -12,6 +12,17 @@
<template v-else>
<!-- Filter Controls -->
<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">
<option value="">All Asset Types</option>
<option v-for="t in assetTypes" :key="t.assettypeid" :value="t.assettype">
@@ -60,6 +71,7 @@
</div>
<ShopFloorMap
:levelid="shownLevelId"
:machines="filteredAssets"
:machinetypes="[]"
:businessunits="businessunits"
@@ -82,7 +94,8 @@ import ShopFloorMap from '../components/ShopFloorMap.vue'
import { assetsApi } from '../api'
import { currentTheme } from '../stores/theme'
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 { assetTypeLabel, assetDetailRoute } from '../utils/assetTypes'
import { getSubtypeId } from '../utils/mapColors'
@@ -104,6 +117,41 @@ const selectedSubtype = ref('')
const selectedBusinessUnit = ref('')
const selectedStatus = 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)
let searchTimeout = null
@@ -245,12 +293,12 @@ async function exportPdf() {
// blueprintUrlFor applies withBase - the raw setting value is a
// root-relative /api path, which 404s under a subpath mount like /ops.
// The PDF always uses the light blueprint: it prints on white paper.
blueprintUrl: blueprintUrlFor('light', mapConfig.currentlevelid),
mapWidth: dimensionsFor(mapConfig.currentlevelid).width,
mapHeight: dimensionsFor(mapConfig.currentlevelid).height,
blueprintUrl: blueprintUrlFor('light', shownLevelId.value),
mapWidth: dimensionsFor(shownLevelId.value).width,
mapHeight: dimensionsFor(shownLevelId.value).height,
// 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.
levelname: levelName(mapConfig.currentlevelid),
levelname: levelName(shownLevelId.value),
selectedType: selectedType.value,
subtypeColors: subtypeColorMap.value,
subtypeNames: subtypeNameMap.value,
@@ -265,7 +313,8 @@ async function exportPdf() {
}
onMounted(async () => {
loadMapConfig()
await loadMapConfig()
shownLevelId.value = mapConfig.currentlevelid ?? mapConfig.defaultlevelid ?? null
try {
const response = await assetsApi.getMap()
const data = response.data.data || {}
@@ -305,6 +354,7 @@ function updateMapLayers() {
function debouncedSearch() {
clearTimeout(searchTimeout)
searchTimeout = setTimeout(() => {
followSearchAcrossLevels()
updateMapLayers()
}, 300)
}
@@ -318,7 +368,7 @@ function handleMarkerClick(asset) {
.map-page {
display: flex;
flex-direction: column;
height: calc(100vh - 2rem);
height: calc(100vh - var(--main-pad-top) - var(--main-pad-bottom));
}
.map-page .page-header {

View File

@@ -213,9 +213,18 @@ async function search(q) {
}
function performSearch() {
if (searchInput.value.trim()) {
router.push({ path: '/search', query: { q: searchInput.value.trim() } })
const term = 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) {

View File

@@ -62,18 +62,36 @@
/>
</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">
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>
<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>
<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) {
const file = event.target.files?.[0]
if (!file) return
@@ -398,6 +439,8 @@ async function remove(level) {
.mono { font-family: monospace; }
.order-input { width: 4.5rem; }
.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; }
.map-thumb {

View File

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

View File

@@ -23,6 +23,14 @@ logger = logging.getLogger(__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):
"""SQL clause matching rows that contain EVERY word of the query, each word
in any of the columns, in any order.
@@ -213,7 +221,7 @@ def _search_applications(query, search_term):
apps = Application.query.filter(
Application.isactive == True,
_word_match(query, Application.appname, Application.appdescription)
).limit(10).all()
).order_by(Application.appname, Application.appid).limit(10).all()
for app in apps:
relevance = 20
@@ -258,7 +266,8 @@ def _search_knowledgebase(query, search_term):
KnowledgeBase.appid.notin_(retired)),
_word_match(query, KnowledgeBase.shortdescription,
KnowledgeBase.keywords)
).limit(20).all()
).order_by(KnowledgeBase.clicks.desc(),
KnowledgeBase.linkid).limit(20).all()
for kb in kb_articles:
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,
Asset.serialnumber, Asset.notes,
Asset.gaugelabreference, Asset.maintenancereference)
).limit(15).all()
).order_by(Asset.assetnumber, Asset.assetid).limit(15).all()
for asset in assets:
relevance = 15
@@ -421,7 +430,7 @@ def _search_measuringtools(query, search_term):
Asset.isactive == True,
_word_match(query, Asset.assetnumber, Asset.name,
Asset.serialnumber, Asset.gaugelabreference)
).limit(15).all()
).order_by(Asset.assetnumber, Asset.assetid).limit(15).all()
for asset in assets:
relevance = 15
@@ -461,7 +470,8 @@ def _search_usbdevices(query, search_term):
USBDevice.isactive == True,
_word_match(query, USBDevice.serialnumber, USBDevice.assetnumber,
USBDevice.label, USBDevice.productname)
).limit(10).all()
).order_by(USBDevice.serialnumber,
USBDevice.usbdeviceid).limit(10).all()
for device in devices:
relevance = 20
@@ -507,7 +517,8 @@ def _search_printeditems(query, search_term):
PrintedItem.isactive == True,
_word_match(query, PrintedItem.itemcode, PrintedItem.gagelabtag,
PrintedItem.itemname, PrintedItem.itemdescription)
).limit(10).all()
).order_by(PrintedItem.itemcode,
PrintedItem.printeditemid).limit(10).all()
for item in items:
relevance = 20
@@ -563,7 +574,7 @@ def _search_customfields(query, search_term):
CustomField.searchable == True,
CustomField.isactive == True,
_word_match(query, CustomFieldValue.value),
).limit(15).all()
).order_by(Asset.assetnumber, CustomField.fieldid).limit(15).all()
for asset, field in rows:
result = _get_asset_result(asset, query, relevance=40)
@@ -583,7 +594,8 @@ def _search_by_ip(query, search_term):
).options(
joinedload(Communication.asset).joinedload(Asset.assettype),
joinedload(Communication.asset).joinedload(Asset.location),
).limit(10).all()
).order_by(Communication.ipaddress,
Communication.communicationid).limit(10).all()
seen_assets = set()
for comm in comms:
@@ -643,7 +655,7 @@ def _search_hostnames(query, search_term):
).options(
joinedload(Computer.asset).joinedload(Asset.assettype),
joinedload(Computer.asset).joinedload(Asset.location),
).limit(10).all()
).order_by(Computer.hostname, Computer.computerid).limit(10).all()
for comp in computers:
if comp.asset and comp.asset.isactive:
@@ -666,7 +678,7 @@ def _search_hostnames(query, search_term):
).options(
joinedload(Printer.asset).joinedload(Asset.assettype),
joinedload(Printer.asset).joinedload(Asset.location),
).limit(10).all()
).order_by(Printer.hostname, Printer.printerid).limit(10).all()
for printer in printers:
if printer.asset and printer.asset.isactive:
@@ -689,7 +701,8 @@ def _search_hostnames(query, search_term):
).options(
joinedload(NetworkDevice.asset).joinedload(Asset.assettype),
joinedload(NetworkDevice.asset).joinedload(Asset.location),
).limit(10).all()
).order_by(NetworkDevice.hostname,
NetworkDevice.networkdeviceid).limit(10).all()
for device in devices:
if device.asset and device.asset.isactive:
@@ -773,7 +786,7 @@ def _search_vendor_model_type(query, search_term):
Asset.isactive == True,
_word_match(query, Vendor.vendor, Model.modelnumber,
MachineType.machinetype)
).limit(10).all()
).order_by(Asset.assetnumber, Asset.assetid).limit(10).all()
for asset in machine_assets:
results.append(_get_asset_result(asset, query, 30))
@@ -801,7 +814,7 @@ def _search_vendor_model_type(query, search_term):
Asset.isactive == True,
_word_match(query, Vendor.vendor, Model.modelnumber,
PrinterType.printertype)
).limit(10).all()
).order_by(Asset.assetnumber, Asset.assetid).limit(10).all()
for asset in printer_assets:
results.append(_get_asset_result(asset, query, 30))
@@ -827,7 +840,7 @@ def _search_vendor_model_type(query, search_term):
Asset.isactive == True,
_word_match(query, Vendor.vendor,
NetworkDeviceType.networkdevicetype)
).limit(10).all()
).order_by(Asset.assetnumber, Asset.assetid).limit(10).all()
for asset in netdev_assets:
results.append(_get_asset_result(asset, query, 30))