Remediate the 2026-09-03 feature-test findings
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user