Name the record in the browser tab, not just the section
Every detail page read "West Jefferson ShopDB - Machine". Two tabs on two different machines were indistinguishable, a bookmark said nothing about what it pointed at, and a link shared into Teams previewed as the section rather than the thing. The router cannot fix this alone: afterEach runs before the view has fetched anything, so it only ever knows the KIND of page. So a detail view names its record once the record arrives, and both writers go through composables/pageTitle.js. THE ORDERING IS THE WHOLE PROBLEM. The router awaits the site settings to build the "<facility> ShopDB" prefix, so on a cold load it finishes AFTER a view that fetched quickly and would overwrite the good title with the generic one. The route's fullPath is claimed before that await, and the generic write stands down when a record title already belongs to the route being landed on. The reverse case is handled too: a view that names its record BEFORE the facility name arrives composed against the placeholder prefix, so the late resolve rewrites it rather than leaving "ShopDB - CSF16" on a site called West Jefferson. Both are pinned by tests, because both fail only under timing. Covers machines, PCs, printers, network devices, subnets, measuring tools, USB devices, printed parts, applications, employees, knowledge base articles and guides. Printers reuse the page's own displayTitle, which already discards the literal name "NONE" a discovered queue can carry, so the tab and the heading agree. A record with no usable name, or one that failed to load, keeps the section name: "Machine" beats "Machine 4192" for somebody scanning a row of tabs. Long names are cut at a word boundary - knowledge base articles are named with a whole sentence of description, which a browser otherwise truncates mid-word. Reports gained meta.title, which the router already supported and nothing used. That also fixes "Pc Relationships", which the name-derived spelling produced.
This commit is contained in:
91
frontend/src/composables/pageTitle.js
Normal file
91
frontend/src/composables/pageTitle.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
// The browser tab title, including WHICH record is open.
|
||||||
|
//
|
||||||
|
// The router can only name the KIND of page, because it runs before the view
|
||||||
|
// has fetched anything: every application read "West Jefferson ShopDB -
|
||||||
|
// Application", so two tabs on two different machines were indistinguishable,
|
||||||
|
// and a shared link previewed as the section rather than the thing.
|
||||||
|
//
|
||||||
|
// So a detail view names itself once its record arrives:
|
||||||
|
//
|
||||||
|
// setRecordTitle(machine.assetnumber)
|
||||||
|
//
|
||||||
|
// THE ORDERING IS THE WHOLE PROBLEM. The router's afterEach awaits the site
|
||||||
|
// settings before it can build "<facility> ShopDB - <page>", so on a cold load
|
||||||
|
// it finishes AFTER a view that fetched quickly has already set a good title,
|
||||||
|
// and would overwrite it with the generic one. Both writers therefore go
|
||||||
|
// through here, and the generic write is dropped when a record title already
|
||||||
|
// belongs to the route being landed on. The key is the route's fullPath, so
|
||||||
|
// navigating away invalidates the previous record's claim.
|
||||||
|
|
||||||
|
import { getFacilityName } from '@/utils/siteSettings'
|
||||||
|
|
||||||
|
let activeKey = null // fullPath of the route the tab is currently showing
|
||||||
|
let recordTitle = null // record name set by a view for activeKey, if any
|
||||||
|
let sitePrefix = 'ShopDB' // '<facility> ShopDB', resolved once and cached
|
||||||
|
|
||||||
|
function compose(page) {
|
||||||
|
return page && page !== 'Home' ? `${sitePrefix} - ${page}` : sitePrefix
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveSitePrefix() {
|
||||||
|
try {
|
||||||
|
const facility = await getFacilityName()
|
||||||
|
if (facility && facility !== 'ShopDB') sitePrefix = `${facility} ShopDB`
|
||||||
|
} catch (settingsError) {
|
||||||
|
// settings unavailable: plain ShopDB is a fine tab title
|
||||||
|
}
|
||||||
|
return sitePrefix
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called by the router on every navigation, BEFORE it awaits anything, so a
|
||||||
|
// view that resolves first is not treated as belonging to the previous route.
|
||||||
|
export function beginNavigation(key) {
|
||||||
|
activeKey = key
|
||||||
|
recordTitle = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called by the router once it knows the site prefix. Skipped when a view has
|
||||||
|
// already named the record for this route.
|
||||||
|
export async function applyGenericTitle(key, page) {
|
||||||
|
await resolveSitePrefix()
|
||||||
|
if (key !== activeKey) return // navigated on while we awaited
|
||||||
|
// A view that named the record before the facility name arrived composed its
|
||||||
|
// title against the placeholder prefix, so rewrite it rather than leaving
|
||||||
|
// 'ShopDB - CSF16' on a site called West Jefferson.
|
||||||
|
document.title = compose(recordTitle !== null ? recordTitle : page)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A tab shows very little, and the browser truncates mid-word wherever it runs
|
||||||
|
// out. Cut at a word boundary instead, so what survives is readable. Knowledge
|
||||||
|
// base articles are the case that forces this: their name is a whole sentence
|
||||||
|
// of description, not a short label.
|
||||||
|
const MAXLENGTH = 60
|
||||||
|
|
||||||
|
function shorten(text) {
|
||||||
|
if (text.length <= MAXLENGTH) return text
|
||||||
|
const cut = text.slice(0, MAXLENGTH)
|
||||||
|
const space = cut.lastIndexOf(' ')
|
||||||
|
return `${(space > MAXLENGTH / 2 ? cut.slice(0, space) : cut).trimEnd()}...`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called by a detail view once its record has loaded. An empty or missing name
|
||||||
|
// leaves the generic title in place rather than showing a bare id: "Machine"
|
||||||
|
// beats "Machine 4192" for somebody scanning a row of tabs, and a record that
|
||||||
|
// failed to load has no name to show anyway.
|
||||||
|
export function setRecordTitle(name) {
|
||||||
|
const text = (name === null || name === undefined) ? '' : String(name).trim()
|
||||||
|
if (!text) return
|
||||||
|
recordTitle = shorten(text)
|
||||||
|
document.title = compose(recordTitle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exposed for tests.
|
||||||
|
export function _state() {
|
||||||
|
return { activeKey, recordTitle, sitePrefix }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function _reset() {
|
||||||
|
activeKey = null
|
||||||
|
recordTitle = null
|
||||||
|
sitePrefix = 'ShopDB'
|
||||||
|
}
|
||||||
177
frontend/src/composables/pageTitle.spec.js
Normal file
177
frontend/src/composables/pageTitle.spec.js
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
// The tab title names the record, and the two writers do not fight.
|
||||||
|
//
|
||||||
|
// The router can only name the KIND of page - it runs before the view has
|
||||||
|
// fetched anything - so a detail view names the record itself once it loads.
|
||||||
|
// That leaves two writers for one string, and the router's is asynchronous
|
||||||
|
// (it awaits the site settings), so on a cold load it can finish LAST and
|
||||||
|
// overwrite a good title with the generic one. These pin that ordering.
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||||
|
|
||||||
|
// The real getFacilityName caches, so only the first call is slow and every
|
||||||
|
// later one is instant. Mirror that: pending calls are held until the test
|
||||||
|
// releases them, and the value is served synchronously from then on. A mock
|
||||||
|
// that hands out a fresh unresolved promise per call hangs the moment a test
|
||||||
|
// navigates twice.
|
||||||
|
let pending = []
|
||||||
|
let settled = null
|
||||||
|
|
||||||
|
function resolveFacility(value) {
|
||||||
|
settled = value
|
||||||
|
const waiting = pending
|
||||||
|
pending = []
|
||||||
|
waiting.forEach((resolve) => resolve(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mock('@/utils/siteSettings', () => ({
|
||||||
|
getFacilityName: () => (settled !== null
|
||||||
|
? Promise.resolve(settled)
|
||||||
|
: new Promise((resolve) => { pending.push(resolve) })),
|
||||||
|
}))
|
||||||
|
|
||||||
|
import {
|
||||||
|
beginNavigation, applyGenericTitle, setRecordTitle, _reset,
|
||||||
|
} from './pageTitle'
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
_reset()
|
||||||
|
document.title = ''
|
||||||
|
pending = []
|
||||||
|
settled = null
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('the generic title', () => {
|
||||||
|
it('names the section when no record has been set', async () => {
|
||||||
|
beginNavigation('/machines')
|
||||||
|
const done = applyGenericTitle('/machines', 'Machines')
|
||||||
|
resolveFacility('West Jefferson')
|
||||||
|
await done
|
||||||
|
|
||||||
|
expect(document.title).toBe('West Jefferson ShopDB - Machines')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to plain ShopDB when the facility is unset', async () => {
|
||||||
|
beginNavigation('/machines')
|
||||||
|
const done = applyGenericTitle('/machines', 'Machines')
|
||||||
|
resolveFacility('ShopDB')
|
||||||
|
await done
|
||||||
|
|
||||||
|
expect(document.title).toBe('ShopDB - Machines')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('drops the site name entirely on the dashboard', async () => {
|
||||||
|
beginNavigation('/')
|
||||||
|
const done = applyGenericTitle('/', 'Home')
|
||||||
|
resolveFacility('West Jefferson')
|
||||||
|
await done
|
||||||
|
|
||||||
|
expect(document.title).toBe('West Jefferson ShopDB')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('a record title', () => {
|
||||||
|
it('names the record once the view has loaded it', async () => {
|
||||||
|
beginNavigation('/machines/12')
|
||||||
|
const done = applyGenericTitle('/machines/12', 'Machines')
|
||||||
|
resolveFacility('West Jefferson')
|
||||||
|
await done
|
||||||
|
|
||||||
|
setRecordTitle('CSF16')
|
||||||
|
|
||||||
|
expect(document.title).toBe('West Jefferson ShopDB - CSF16')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('SURVIVES a slow router that resolves afterwards', async () => {
|
||||||
|
// The regression this file exists for. The view wins the race, the
|
||||||
|
// settings fetch lands second, and the generic write must stand down.
|
||||||
|
beginNavigation('/machines/12')
|
||||||
|
const done = applyGenericTitle('/machines/12', 'Machines')
|
||||||
|
|
||||||
|
setRecordTitle('CSF16') // view got there first
|
||||||
|
resolveFacility('West Jefferson')
|
||||||
|
await done // router finishes last
|
||||||
|
|
||||||
|
expect(document.title).toBe('West Jefferson ShopDB - CSF16')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('is ignored when the record has no usable name', async () => {
|
||||||
|
beginNavigation('/machines/12')
|
||||||
|
const done = applyGenericTitle('/machines/12', 'Machines')
|
||||||
|
resolveFacility('West Jefferson')
|
||||||
|
await done
|
||||||
|
|
||||||
|
setRecordTitle('')
|
||||||
|
setRecordTitle(null)
|
||||||
|
setRecordTitle(undefined)
|
||||||
|
setRecordTitle(' ')
|
||||||
|
|
||||||
|
// the section name is a better tab than a blank or a bare id
|
||||||
|
expect(document.title).toBe('West Jefferson ShopDB - Machines')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shortens a name too long for a tab, at a word boundary', async () => {
|
||||||
|
// Knowledge base articles are named with a sentence of description, and a
|
||||||
|
// browser truncates those mid-word wherever it happens to run out.
|
||||||
|
beginNavigation('/knowledgebase/2')
|
||||||
|
const done = applyGenericTitle('/knowledgebase/2', 'Knowledge Base')
|
||||||
|
resolveFacility('West Jefferson')
|
||||||
|
await done
|
||||||
|
|
||||||
|
const full = 'Documentation on how to image a Standard or Business PC '
|
||||||
|
+ 'in GCC High using MediaCreator Lite'
|
||||||
|
setRecordTitle(full)
|
||||||
|
|
||||||
|
expect(document.title.length).toBeLessThan(90)
|
||||||
|
expect(document.title.startsWith('West Jefferson ShopDB - ')).toBe(true)
|
||||||
|
expect(document.title.endsWith('...')).toBe(true)
|
||||||
|
|
||||||
|
// what survives is a whole-word prefix of the original: the character
|
||||||
|
// following the cut in the source is a space, so no word was split
|
||||||
|
const kept = document.title
|
||||||
|
.replace('West Jefferson ShopDB - ', '')
|
||||||
|
.replace(/\.\.\.$/, '')
|
||||||
|
expect(full.startsWith(kept)).toBe(true)
|
||||||
|
expect(full[kept.length]).toBe(' ')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('leaves a name that already fits alone', async () => {
|
||||||
|
beginNavigation('/machines/14')
|
||||||
|
const done = applyGenericTitle('/machines/14', 'Machines')
|
||||||
|
resolveFacility('West Jefferson')
|
||||||
|
await done
|
||||||
|
|
||||||
|
setRecordTitle('0600')
|
||||||
|
|
||||||
|
expect(document.title).toBe('West Jefferson ShopDB - 0600')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not leak onto the next page', async () => {
|
||||||
|
beginNavigation('/machines/12')
|
||||||
|
const first = applyGenericTitle('/machines/12', 'Machines')
|
||||||
|
resolveFacility('West Jefferson')
|
||||||
|
await first
|
||||||
|
setRecordTitle('CSF16')
|
||||||
|
|
||||||
|
beginNavigation('/printers')
|
||||||
|
const second = applyGenericTitle('/printers', 'Printers')
|
||||||
|
await second
|
||||||
|
|
||||||
|
expect(document.title).toBe('West Jefferson ShopDB - Printers')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('is discarded when the user navigates away mid-flight', async () => {
|
||||||
|
// Two navigations overlap; the first one's late generic write must not
|
||||||
|
// land on the second page.
|
||||||
|
beginNavigation('/machines/12')
|
||||||
|
const stale = applyGenericTitle('/machines/12', 'Machines')
|
||||||
|
|
||||||
|
beginNavigation('/printers/3')
|
||||||
|
const fresh = applyGenericTitle('/printers/3', 'Printers')
|
||||||
|
setRecordTitle('WJPRT04')
|
||||||
|
|
||||||
|
resolveFacility('West Jefferson')
|
||||||
|
await Promise.all([stale, fresh])
|
||||||
|
|
||||||
|
expect(document.title).toBe('West Jefferson ShopDB - WJPRT04')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -6,7 +6,7 @@ import { setupComplete, setupSkipped, isSetupLoaded, refreshSetupState,
|
|||||||
needsAdmin, isNeedsAdminLoaded, refreshNeedsAdmin } from '../composables/setupState'
|
needsAdmin, isNeedsAdminLoaded, refreshNeedsAdmin } from '../composables/setupState'
|
||||||
import { loadEnabledPlugins, isPluginEnabled } from '../composables/enabledPlugins'
|
import { loadEnabledPlugins, isPluginEnabled } from '../composables/enabledPlugins'
|
||||||
import { useToast } from '../composables/toast'
|
import { useToast } from '../composables/toast'
|
||||||
import { getFacilityName } from '../utils/siteSettings'
|
import { beginNavigation, applyGenericTitle } from '../composables/pageTitle'
|
||||||
import { stagedChildren, stagedToplevel } from './routes.gen'
|
import { stagedChildren, stagedToplevel } from './routes.gen'
|
||||||
|
|
||||||
// Auto-discover all route modules from routes/ directory. A module's default
|
// Auto-discover all route modules from routes/ directory. A module's default
|
||||||
@@ -147,14 +147,12 @@ function pageTitleFor(route) {
|
|||||||
word.charAt(0).toUpperCase() + word.slice(1)).join(' ')
|
word.charAt(0).toUpperCase() + word.slice(1)).join(' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
router.afterEach(async (to) => {
|
router.afterEach((to) => {
|
||||||
let siteTitle = 'ShopDB'
|
// Claim the route BEFORE awaiting anything: a detail view that resolves
|
||||||
try {
|
// quickly sets the record title, and the generic write below must not
|
||||||
const facility = await getFacilityName()
|
// overwrite it. See composables/pageTitle.js.
|
||||||
if (facility && facility !== 'ShopDB') siteTitle = `${facility} ShopDB`
|
beginNavigation(to.fullPath)
|
||||||
} catch (titleError) { /* settings unavailable - plain ShopDB */ }
|
applyGenericTitle(to.fullPath, pageTitleFor(to))
|
||||||
const page = pageTitleFor(to)
|
|
||||||
document.title = page && page !== 'Home' ? `${siteTitle} - ${page}` : siteTitle
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// A deploy replaces the hashed chunk files, so a tab that was open across it
|
// A deploy replaces the hashed chunk files, so a tab that was open across it
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ import { applicationsApi } from '@/api'
|
|||||||
import { getContactEmailDomain } from '@/utils/siteSettings'
|
import { getContactEmailDomain } from '@/utils/siteSettings'
|
||||||
import { sanitizeNotesHtml } from '@/utils/sanitizeHtml'
|
import { sanitizeNotesHtml } from '@/utils/sanitizeHtml'
|
||||||
import { fileHref } from '@/utils/basePath'
|
import { fileHref } from '@/utils/basePath'
|
||||||
|
import { setRecordTitle } from '@/composables/pageTitle'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
@@ -202,6 +203,7 @@ onMounted(async () => {
|
|||||||
// Load application details
|
// Load application details
|
||||||
const response = await applicationsApi.get(route.params.id)
|
const response = await applicationsApi.get(route.params.id)
|
||||||
app.value = response.data.data
|
app.value = response.data.data
|
||||||
|
setRecordTitle(app.value?.appname)
|
||||||
|
|
||||||
// Load versions
|
// Load versions
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default [
|
|||||||
path: 'reports/pc-relationships',
|
path: 'reports/pc-relationships',
|
||||||
name: 'report-pc-relationships',
|
name: 'report-pc-relationships',
|
||||||
component: () => import('./views/PCRelationshipsReport.vue'),
|
component: () => import('./views/PCRelationshipsReport.vue'),
|
||||||
meta: { plugin: 'computers' }
|
meta: { plugin: 'computers', title: 'PC Relationships Report' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'settings/pctypemapping',
|
path: 'settings/pctypemapping',
|
||||||
|
|||||||
@@ -263,6 +263,7 @@ import CustomFieldsSection from '@/components/CustomFieldsSection.vue'
|
|||||||
import PluginAssetPanels from '@/components/PluginAssetPanels.vue'
|
import PluginAssetPanels from '@/components/PluginAssetPanels.vue'
|
||||||
import AssetRelationships from '@/components/AssetRelationships.vue'
|
import AssetRelationships from '@/components/AssetRelationships.vue'
|
||||||
import { useIdentifierFlags } from '@/composables/identifierSettings'
|
import { useIdentifierFlags } from '@/composables/identifierSettings'
|
||||||
|
import { setRecordTitle } from '@/composables/pageTitle'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { isEnabled } = useIdentifierFlags()
|
const { isEnabled } = useIdentifierFlags()
|
||||||
@@ -280,6 +281,7 @@ onMounted(async () => {
|
|||||||
try {
|
try {
|
||||||
const response = await computersApi.get(route.params.id)
|
const response = await computersApi.get(route.params.id)
|
||||||
computer.value = response.data.data
|
computer.value = response.data.data
|
||||||
|
setRecordTitle(computer.value?.assetnumber || computer.value?.name)
|
||||||
|
|
||||||
// Load installed applications
|
// Load installed applications
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ import { withBase } from '@/utils/basePath'
|
|||||||
import { employeesApi, usbApi, notificationsApi } from '@/api'
|
import { employeesApi, usbApi, notificationsApi } from '@/api'
|
||||||
import { isPluginEnabled, loadEnabledPlugins } from '@/composables/enabledPlugins'
|
import { isPluginEnabled, loadEnabledPlugins } from '@/composables/enabledPlugins'
|
||||||
import { useToast } from '@/composables/toast'
|
import { useToast } from '@/composables/toast'
|
||||||
|
import { setRecordTitle } from '@/composables/pageTitle'
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -223,6 +224,7 @@ async function loadEmployee() {
|
|||||||
try {
|
try {
|
||||||
const response = await employeesApi.lookup(route.params.sso)
|
const response = await employeesApi.lookup(route.params.sso)
|
||||||
employee.value = response.data.data
|
employee.value = response.data.data
|
||||||
|
setRecordTitle(employee.value?.name || employee.value?.sso)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading employee:', err)
|
console.error('Error loading employee:', err)
|
||||||
error.value = 'Employee not found'
|
error.value = 'Employee not found'
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import { useRoute } from 'vue-router'
|
|||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { apiError } from '@/utils/apiError'
|
import { apiError } from '@/utils/apiError'
|
||||||
import GuideBlocks from '../components/GuideBlocks.vue'
|
import GuideBlocks from '../components/GuideBlocks.vue'
|
||||||
|
import { setRecordTitle } from '@/composables/pageTitle'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const guide = ref(null)
|
const guide = ref(null)
|
||||||
@@ -146,6 +147,7 @@ async function load () {
|
|||||||
try {
|
try {
|
||||||
const response = await api.get('/guides/' + route.params.slug)
|
const response = await api.get('/guides/' + route.params.slug)
|
||||||
guide.value = response.data?.data || response.data
|
guide.value = response.data?.data || response.data
|
||||||
|
setRecordTitle(guide.value?.title)
|
||||||
loadProgress()
|
loadProgress()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = apiError(err, 'Could not load this guide')
|
error.value = apiError(err, 'Could not load this guide')
|
||||||
|
|||||||
@@ -80,6 +80,7 @@
|
|||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { knowledgebaseApi } from '@/api'
|
import { knowledgebaseApi } from '@/api'
|
||||||
|
import { setRecordTitle } from '@/composables/pageTitle'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
@@ -90,6 +91,7 @@ onMounted(async () => {
|
|||||||
try {
|
try {
|
||||||
const response = await knowledgebaseApi.get(route.params.id)
|
const response = await knowledgebaseApi.get(route.params.id)
|
||||||
article.value = response.data.data
|
article.value = response.data.data
|
||||||
|
setRecordTitle(article.value?.shortdescription)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading article:', error)
|
console.error('Error loading article:', error)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -257,6 +257,7 @@ import PluginAssetPanels from '@/components/PluginAssetPanels.vue'
|
|||||||
import AssetRelationships from '@/components/AssetRelationships.vue'
|
import AssetRelationships from '@/components/AssetRelationships.vue'
|
||||||
import { useWarrantyBadge } from '@/composables/warrantyBadge'
|
import { useWarrantyBadge } from '@/composables/warrantyBadge'
|
||||||
import { useIdentifierFlags } from '@/composables/identifierSettings'
|
import { useIdentifierFlags } from '@/composables/identifierSettings'
|
||||||
|
import { setRecordTitle } from '@/composables/pageTitle'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { isEnabled } = useIdentifierFlags()
|
const { isEnabled } = useIdentifierFlags()
|
||||||
@@ -271,6 +272,7 @@ onMounted(async () => {
|
|||||||
try {
|
try {
|
||||||
const response = await machinesApi.get(route.params.id)
|
const response = await machinesApi.get(route.params.id)
|
||||||
machine.value = response.data.data
|
machine.value = response.data.data
|
||||||
|
setRecordTitle(machine.value?.assetnumber || machine.value?.name)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading machine:', error)
|
console.error('Error loading machine:', error)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default [
|
|||||||
path: 'reports/calibration',
|
path: 'reports/calibration',
|
||||||
name: 'calibration-report',
|
name: 'calibration-report',
|
||||||
component: () => import('./views/CalibrationReport.vue'),
|
component: () => import('./views/CalibrationReport.vue'),
|
||||||
meta: { plugin: 'measuringtools' }
|
meta: { plugin: 'measuringtools', title: 'Calibration Report' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'settings/measuringtooltypes',
|
path: 'settings/measuringtooltypes',
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ import CustomFieldsSection from '@/components/CustomFieldsSection.vue'
|
|||||||
import PluginAssetPanels from '@/components/PluginAssetPanels.vue'
|
import PluginAssetPanels from '@/components/PluginAssetPanels.vue'
|
||||||
import AssetRelationships from '@/components/AssetRelationships.vue'
|
import AssetRelationships from '@/components/AssetRelationships.vue'
|
||||||
import { useIdentifierFlags } from '@/composables/identifierSettings'
|
import { useIdentifierFlags } from '@/composables/identifierSettings'
|
||||||
|
import { setRecordTitle } from '@/composables/pageTitle'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
@@ -193,6 +194,7 @@ onMounted(async () => {
|
|||||||
? await measuringtoolsApi.getByAsset(route.params.assetid)
|
? await measuringtoolsApi.getByAsset(route.params.assetid)
|
||||||
: await measuringtoolsApi.get(route.params.id)
|
: await measuringtoolsApi.get(route.params.id)
|
||||||
tool.value = response.data.data
|
tool.value = response.data.data
|
||||||
|
setRecordTitle(tool.value?.assetnumber || tool.value?.name)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading measuring tool:', err)
|
console.error('Error loading measuring tool:', err)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -228,6 +228,7 @@ import PluginAssetPanels from '@/components/PluginAssetPanels.vue'
|
|||||||
import { useWarrantyBadge } from '@/composables/warrantyBadge'
|
import { useWarrantyBadge } from '@/composables/warrantyBadge'
|
||||||
import { useIdentifierFlags } from '@/composables/identifierSettings'
|
import { useIdentifierFlags } from '@/composables/identifierSettings'
|
||||||
import { useToast } from '@/composables/toast'
|
import { useToast } from '@/composables/toast'
|
||||||
|
import { setRecordTitle } from '@/composables/pageTitle'
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const { isEnabled } = useIdentifierFlags()
|
const { isEnabled } = useIdentifierFlags()
|
||||||
@@ -250,6 +251,7 @@ async function loadDevice() {
|
|||||||
try {
|
try {
|
||||||
const response = await networkApi.get(deviceId)
|
const response = await networkApi.get(deviceId)
|
||||||
device.value = response.data.data
|
device.value = response.data.data
|
||||||
|
setRecordTitle(device.value?.assetnumber || device.value?.name)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading device:', error)
|
console.error('Error loading device:', error)
|
||||||
device.value = null
|
device.value = null
|
||||||
|
|||||||
@@ -59,6 +59,7 @@
|
|||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { networkApi } from '@/api'
|
import { networkApi } from '@/api'
|
||||||
|
import { setRecordTitle } from '@/composables/pageTitle'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const subnet = ref(null)
|
const subnet = ref(null)
|
||||||
@@ -79,6 +80,7 @@ async function load() {
|
|||||||
try {
|
try {
|
||||||
const response = await networkApi.subnets.get(route.params.id)
|
const response = await networkApi.subnets.get(route.params.id)
|
||||||
subnet.value = response.data?.data || response.data
|
subnet.value = response.data?.data || response.data
|
||||||
|
setRecordTitle(subnet.value?.name || subnet.value?.cidr)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ import { useRoute } from 'vue-router'
|
|||||||
import { printedpartsApi } from '@/api'
|
import { printedpartsApi } from '@/api'
|
||||||
import { withBase } from '@/utils/basePath'
|
import { withBase } from '@/utils/basePath'
|
||||||
import Modal from '@/components/Modal.vue'
|
import Modal from '@/components/Modal.vue'
|
||||||
|
import { setRecordTitle } from '@/composables/pageTitle'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const item = ref(null)
|
const item = ref(null)
|
||||||
@@ -196,6 +197,7 @@ onMounted(async () => {
|
|||||||
try {
|
try {
|
||||||
const response = await printedpartsApi.get(route.params.id)
|
const response = await printedpartsApi.get(route.params.id)
|
||||||
item.value = response.data.data
|
item.value = response.data.data
|
||||||
|
setRecordTitle(item.value?.itemname || item.value?.itemcode)
|
||||||
await loadFiles()
|
await loadFiles()
|
||||||
} catch (loadError) {
|
} catch (loadError) {
|
||||||
console.error('Error loading printed item:', loadError)
|
console.error('Error loading printed item:', loadError)
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ export default [
|
|||||||
path: 'reports/toner',
|
path: 'reports/toner',
|
||||||
name: 'toner-report',
|
name: 'toner-report',
|
||||||
component: () => import('./views/TonerReport.vue'),
|
component: () => import('./views/TonerReport.vue'),
|
||||||
meta: { plugin: 'printers' }
|
meta: { plugin: 'printers', title: 'Toner Report' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'reports/toner-forecast',
|
path: 'reports/toner-forecast',
|
||||||
name: 'toner-forecast',
|
name: 'toner-forecast',
|
||||||
component: () => import('./views/TonerForecast.vue'),
|
component: () => import('./views/TonerForecast.vue'),
|
||||||
meta: { plugin: 'printers' }
|
meta: { plugin: 'printers', title: 'Toner Forecast' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'settings/printertypes',
|
path: 'settings/printertypes',
|
||||||
|
|||||||
@@ -305,6 +305,7 @@ import PluginAssetPanels from '@/components/PluginAssetPanels.vue'
|
|||||||
import AssetRelationships from '@/components/AssetRelationships.vue'
|
import AssetRelationships from '@/components/AssetRelationships.vue'
|
||||||
import { useWarrantyBadge } from '@/composables/warrantyBadge'
|
import { useWarrantyBadge } from '@/composables/warrantyBadge'
|
||||||
import { useIdentifierFlags } from '@/composables/identifierSettings'
|
import { useIdentifierFlags } from '@/composables/identifierSettings'
|
||||||
|
import { setRecordTitle } from '@/composables/pageTitle'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { isEnabled } = useIdentifierFlags()
|
const { isEnabled } = useIdentifierFlags()
|
||||||
@@ -344,6 +345,9 @@ onMounted(async () => {
|
|||||||
])
|
])
|
||||||
|
|
||||||
printer.value = printerRes.data.data
|
printer.value = printerRes.data.data
|
||||||
|
// the page's own heading logic, which already discards the literal
|
||||||
|
// name 'NONE' that a discovered queue can carry
|
||||||
|
setRecordTitle(displayTitle.value)
|
||||||
// supplies endpoint returns {ipaddress, pingstatus, supplies:[...]}
|
// supplies endpoint returns {ipaddress, pingstatus, supplies:[...]}
|
||||||
supplies.value = suppliesRes.data.data?.supplies || []
|
supplies.value = suppliesRes.data.data?.supplies || []
|
||||||
// drivers are attached to the printer detail, matched by model
|
// drivers are attached to the printer detail, matched by model
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ import { usbApi } from '@/api'
|
|||||||
import Modal from '@/components/Modal.vue'
|
import Modal from '@/components/Modal.vue'
|
||||||
import { useToast } from '@/composables/toast'
|
import { useToast } from '@/composables/toast'
|
||||||
import { apiError } from '@/utils/apiError'
|
import { apiError } from '@/utils/apiError'
|
||||||
|
import { setRecordTitle } from '@/composables/pageTitle'
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -181,6 +182,7 @@ async function loadDevice() {
|
|||||||
try {
|
try {
|
||||||
const response = await usbApi.get(route.params.id)
|
const response = await usbApi.get(route.params.id)
|
||||||
device.value = response.data.data
|
device.value = response.data.data
|
||||||
|
setRecordTitle(device.value?.label || device.value?.serialnumber)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading USB device:', error)
|
console.error('Error loading USB device:', error)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export default [
|
|||||||
path: 'reports/warranty',
|
path: 'reports/warranty',
|
||||||
name: 'warranty-report',
|
name: 'warranty-report',
|
||||||
component: () => import('./views/WarrantyReport.vue'),
|
component: () => import('./views/WarrantyReport.vue'),
|
||||||
meta: { plugin: 'warranty' }
|
meta: { plugin: 'warranty', title: 'Warranty Report' }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user