Remediate the 2026-09-03 feature-test findings

This commit is contained in:
2026-09-03 17:15:03 -04:00
parent cf95daf3bb
commit fd513c08eb
112 changed files with 5182 additions and 857 deletions

View File

@@ -143,6 +143,10 @@
v-model="form.locationid"
class="form-control"
>
<!-- Blank means "no location of its own", which is a real state:
the PC then follows whatever it is attached to. The hint
below says where that currently resolves to, so leaving the
box empty is an informed choice rather than a missing one. -->
<option value="">Select location...</option>
<option
v-for="l in locations"
@@ -152,6 +156,33 @@
{{ l.locationname }}
</option>
</select>
<small v-if="!form.locationid && inheritedLocation" class="input-hint">
No location of its own. It currently shows
<strong>{{ inheritedLocation.locationname }}</strong>, inherited
from {{ inheritedLocation.inheritedfrom }}. Pick one here only to
override that.
</small>
</div>
<div class="form-group">
<!-- The PC detail page has always rendered a Business Unit row,
and the API has always accepted the field - only the form had
no control, so it could be seen and never set from the UI. -->
<label for="businessunitid">Business Unit</label>
<select
id="businessunitid"
v-model="form.businessunitid"
class="form-control"
>
<option value="">Select business unit...</option>
<option
v-for="bu in businessunits"
:key="bu.businessunitid"
:value="bu.businessunitid"
>
{{ bu.businessunit }}
</option>
</select>
</div>
</div>
@@ -364,7 +395,7 @@
import { ref, onMounted, computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { computersApi, assetsApi, vendorsApi, locationsApi, modelsApi, operatingsystemsApi,
printersApi, relationshipTypesApi } from '@/api'
printersApi, relationshipTypesApi, businessunitsApi } from '@/api'
import ShopFloorMap from '@/components/ShopFloorMap.vue'
import { loadMapConfig, levelOptions, levelName, state as mapConfig }
from '@/composables/mapConfig'
@@ -373,6 +404,7 @@ import CustomFieldsInputs from '@/components/CustomFieldsInputs.vue'
import PrinterAssignmentPicker from '@/components/PrinterAssignmentPicker.vue'
import { currentTheme } from '@/stores/theme'
import { useIdentifierFlags } from '@/composables/identifierSettings'
import { loadAssetTypeIds, assetTypeId } from '@/composables/assetTypeIds'
import { isPluginEnabled, loadEnabledPlugins } from '@/composables/enabledPlugins'
import { useToast } from '@/composables/toast'
import { apiError } from '@/utils/apiError'
@@ -390,13 +422,20 @@ const isEdit = computed(() => !!route.params.id)
// falls back to the PCs list.
const returnTo = computed(() => (route.query.from === 'fleet' ? '/endpoints' : '/pcs'))
// Seeded asset-type id for computers (see /api/assets/types). Custom-field
// values are keyed by the underlying asset id, captured on load / create.
const COMPUTER_ASSETTYPEID = 2
// Resolved by NAME at load, never hardcoded: the id comes from seed
// order, so the constant that used to live here pointed at a different
// asset type on a differently-seeded site - the form then rendered THAT
// type's custom fields and dropped whatever was typed into them.
const COMPUTER_ASSETTYPEID = computed(() => assetTypeId('computer'))
const customFieldsRef = ref(null)
const printerPickerRef = ref(null)
const currentAssetId = ref(null)
// What this PC resolves to today when it owns no location, captured at load.
// Read-only: the form must never post it back as the record's own, which is
// exactly what binding the location select to the merged `locationid` did.
const inheritedLocation = ref(null)
// PC Number (assetnumber) defaults to the serial number while the user hasn't
// typed their own. Editable; only auto-fills on a new PC.
const manualPcNumber = ref(false)
@@ -470,6 +509,7 @@ function setProtocolPort(protocolid, value) {
const vendors = ref([])
const models = ref([])
const locations = ref([])
const businessunits = ref([])
const operatingsystems = ref([])
@@ -519,17 +559,20 @@ const filteredVendors = computed(() => {
})
onMounted(async () => {
await loadAssetTypeIds()
try {
// Load reference data
// perpage 100 so dropdowns aren't truncated to the default 20-row page
const [ptRes, statusRes, vendorRes, allModels, locRes, osRes, protoRes] = await Promise.all([
const [ptRes, statusRes, vendorRes, allModels, locRes, osRes, protoRes,
allBusinessUnits] = await Promise.all([
computersApi.types.list({ perpage: 100 }),
assetsApi.statuses.list(),
vendorsApi.list({ perpage: 100 }),
vendorsApi.listAll(),
modelsApi.listAll({ category: 'PC' }), // backend caps perpage at 100; page through all
locationsApi.listAll(), // backend caps perpage at 100; page through all
operatingsystemsApi.list({ perpage: 100 }),
computersApi.protocols.list(),
businessunitsApi.listAll(),
// Handles its own failure: a site without printers still gets a form.
])
@@ -540,6 +583,7 @@ onMounted(async () => {
locations.value = locRes || []
operatingsystems.value = osRes.data.data || []
protocols.value = protoRes.data.data || []
businessunits.value = allBusinessUnits || []
// Load PC if editing (asset-based shape: extension under pc.computer)
if (isEdit.value) {
@@ -550,6 +594,7 @@ onMounted(async () => {
const primaryComm = pc.communications?.find(c => c.isprimary) || pc.communications?.[0]
inheritedLocation.value = pc.inheritedlocation || null
form.value = {
machinenumber: pc.assetnumber || '',
alias: pc.name && pc.name.toUpperCase() !== 'NONE' ? pc.name : '',
@@ -561,7 +606,12 @@ onMounted(async () => {
statusid: pc.statusid || '',
vendorid: ext.vendorid || '',
modelnumberid: ext.modelnumberid || '',
locationid: pc.locationid || '',
// The OWN location, not the resolved one. Binding to `locationid`
// meant the form pre-selected a location this PC had inherited from a
// machine, and saving wrote it back as the PC's own - so an edit that
// never touched location silently stopped the PC following its parent.
locationid: pc.ownlocationid || '',
businessunitid: pc.businessunitid || '',
osid: ext.osid || '',
loggedinuser: ext.loggedinuser || '',
azuredeviceid: ext.azuredeviceid || '',
@@ -572,9 +622,9 @@ onMounted(async () => {
portoverride: a.portoverride ?? null
})),
notes: pc.notes || '',
mapx: pc.mapx ?? null,
mapy: pc.mapy ?? null,
levelid: pc.levelid ?? null,
mapx: pc.ownmapx ?? null,
mapy: pc.ownmapy ?? null,
levelid: pc.ownlevelid ?? null,
ipaddress: primaryComm?.ipaddress || ''
}
@@ -640,6 +690,7 @@ async function savePC() {
vendorid: form.value.vendorid || null,
modelnumberid: form.value.modelnumberid || null,
locationid: form.value.locationid || null,
businessunitid: form.value.businessunitid || null,
osid: form.value.osid || null,
loggedinuser: form.value.loggedinuser || null,
azuredeviceid: form.value.azuredeviceid || null,
@@ -649,10 +700,22 @@ async function savePC() {
notes: form.value.notes || null,
ipaddress: form.value.ipaddress || null,
mapx: form.value.mapx,
mapy: form.value.mapy
mapy: form.value.mapy,
// ADR-017: a position without its level cannot be drawn on the right
// floor plan. The picker sets all three and this payload sent two, so a
// picked pin saved a levelid of null and then rendered nowhere - the
// form even showed "Position: x, y" back, because it kept the value it
// had never sent.
levelid: form.value.levelid
}
// only set display name when an alias is given, so we don't clobber it
if (form.value.alias) {
// An emptied alias has to be SENT, or it cannot be cleared: the payload
// simply omitted `name`, the old value survived, and the form came back
// showing the alias the operator had just deleted, with no error and no
// toast. On CREATE the field is still omitted when blank, because there
// the server may fill it in and sending null would overwrite that.
if (isEdit.value) {
payload.name = form.value.alias || null
} else if (form.value.alias) {
payload.name = form.value.alias
}