Name the toner on the forecast, keep the number on the hover
This commit is contained in:
@@ -42,7 +42,11 @@
|
|||||||
<ul v-else class="order-list">
|
<ul v-else class="order-list">
|
||||||
<li v-for="item in mapped" :key="orderKey(item)">
|
<li v-for="item in mapped" :key="orderKey(item)">
|
||||||
<span class="quantity">{{ item.quantity }}x</span>
|
<span class="quantity">{{ item.quantity }}x</span>
|
||||||
<span class="partnumber">{{ item.partnumber }}</span>
|
<!-- Name here too, number on hover. 'Copy list' still writes the
|
||||||
|
part number, which is what an order is placed against. -->
|
||||||
|
<span class="supplyname" :title="item.partnumber">
|
||||||
|
{{ item.marketingname || item.partnumber }}
|
||||||
|
</span>
|
||||||
<span class="muted order-detail">
|
<span class="muted order-detail">
|
||||||
{{ colorLabel(item.color) }}<template v-if="item.model">, {{ item.model }}</template>
|
{{ colorLabel(item.color) }}<template v-if="item.model">, {{ item.model }}</template>
|
||||||
</span>
|
</span>
|
||||||
@@ -107,7 +111,16 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>{{ c.name }}</td>
|
<td>{{ c.name }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span v-if="firstPart(c)" class="partnumber">{{ firstPart(c) }}</span>
|
<!-- The cartridge's NAME, with the part number on hover. A
|
||||||
|
name says which toner this is; 'W2020A' only identifies it
|
||||||
|
once you already know. The number is what purchasing wants,
|
||||||
|
so it stays one hover away and stays in the copied order
|
||||||
|
list verbatim. Falls back to the number when a supply has
|
||||||
|
no name on file - marketingname is nullable. -->
|
||||||
|
<span v-if="firstPart(c)" class="supplyname"
|
||||||
|
:title="firstPart(c).partnumber">
|
||||||
|
{{ firstPart(c).marketingname || firstPart(c).partnumber }}
|
||||||
|
</span>
|
||||||
<span v-else class="muted">-</span>
|
<span v-else class="muted">-</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="collevel">
|
<td class="collevel">
|
||||||
@@ -259,9 +272,13 @@ function colorLabel(color) {
|
|||||||
return color.charAt(0).toUpperCase() + color.slice(1)
|
return color.charAt(0).toUpperCase() + color.slice(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The whole part row, not just its number: the table shows the name and keeps
|
||||||
|
// the number for the hover, and both come from here. Several capacity tiers can
|
||||||
|
// match; the first is the standard one, which is what orderlist() groups on and
|
||||||
|
// what the low-supplies report shows first.
|
||||||
function firstPart(cartridge) {
|
function firstPart(cartridge) {
|
||||||
const parts = cartridge.partnumbers || []
|
const parts = cartridge.partnumbers || []
|
||||||
return parts.length ? parts[0].partnumber : null
|
return parts.length ? parts[0] : null
|
||||||
}
|
}
|
||||||
|
|
||||||
function levelText(cartridge) {
|
function levelText(cartridge) {
|
||||||
@@ -376,8 +393,10 @@ onMounted(load)
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
.unmapped-block .order-list li { border-top: 1px dashed var(--border); }
|
.unmapped-block .order-list li { border-top: 1px dashed var(--border); }
|
||||||
.partnumber { font-family: monospace; font-weight: 700; }
|
/* A name is prose, not a code, so it does not get the monospace the part number
|
||||||
.partnumber.unmapped { font-weight: 400; color: var(--warning); }
|
used to carry here. The help cursor is the only signal that the number is on
|
||||||
|
the hover. Replaced .partnumber, which nothing renders any more. */
|
||||||
|
.supplyname { font-weight: 700; cursor: help; }
|
||||||
.order-detail { font-size: 0.9rem; }
|
.order-detail { font-size: 0.9rem; }
|
||||||
.order-printers { font-size: 0.8rem; margin-left: auto; text-align: right; }
|
.order-printers { font-size: 0.8rem; margin-left: auto; text-align: right; }
|
||||||
|
|
||||||
|
|||||||
110
plugins/printers/frontend/views/tonerForecastNames.spec.js
Normal file
110
plugins/printers/frontend/views/tonerForecastNames.spec.js
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest'
|
||||||
|
import { mount, flushPromises } from '@vue/test-utils'
|
||||||
|
|
||||||
|
// The forecast identifies a cartridge by its NAME - '414X Black' - and keeps the
|
||||||
|
// part number on the hover. A part number alone ('W2020X') only identifies the
|
||||||
|
// toner to someone who already knows it, and the two places that name a supply
|
||||||
|
// (the order list and the Part column) had shown nothing else.
|
||||||
|
//
|
||||||
|
// The number has to survive somewhere: it is what an order is placed against.
|
||||||
|
// It stays in the title attribute and, verbatim and unlabelled, in the text that
|
||||||
|
// 'Copy list' writes to the clipboard. These specs pin both halves.
|
||||||
|
//
|
||||||
|
// marketingname is nullable, so every display falls back to the number.
|
||||||
|
|
||||||
|
vi.mock('@/api', () => ({
|
||||||
|
printersApi: { supplyForecast: vi.fn() },
|
||||||
|
}))
|
||||||
|
vi.mock('@/utils/apiError', () => ({ apiError: (e) => String(e) }))
|
||||||
|
|
||||||
|
import { printersApi } from '@/api'
|
||||||
|
import TonerForecast from './TonerForecast.vue'
|
||||||
|
|
||||||
|
function payload({ marketingname = '414X Black', partnumber = 'W2020X' } = {}) {
|
||||||
|
const part = { partnumber, marketingname, capacitytier: 'high', pageyield: 7500 }
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
data: {
|
||||||
|
available: true,
|
||||||
|
days: 90,
|
||||||
|
horizondays: 14,
|
||||||
|
summary: { bands: { empty: 1 }, replacements: 3, toorder: 1 },
|
||||||
|
cartridges: [{
|
||||||
|
printerid: 1, printername: 'GageLab-Xerox-C415', assetnumber: 'P-1',
|
||||||
|
ipaddress: '10.49.215.12', name: 'Black toner', color: 'black',
|
||||||
|
currentlevel: 4, band: 'empty', daysleft: 0, burnrateperday: 1.8,
|
||||||
|
rateunstable: false, replacements: 3, basisdays: 12,
|
||||||
|
partnumbers: [part],
|
||||||
|
}],
|
||||||
|
unestimated: [],
|
||||||
|
orderlist: [{
|
||||||
|
partnumber, marketingname, color: 'black', supplytype: 'toner',
|
||||||
|
model: 'Versalink C415', alternates: [], quantity: 2,
|
||||||
|
printers: [{ printerid: 1, printername: 'GageLab-Xerox-C415', daysleft: 0 }],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function render(overrides) {
|
||||||
|
printersApi.supplyForecast.mockResolvedValue(payload(overrides))
|
||||||
|
const wrapper = mount(TonerForecast, {
|
||||||
|
global: { stubs: { 'router-link': { template: '<a><slot /></a>' } } },
|
||||||
|
})
|
||||||
|
await flushPromises()
|
||||||
|
return wrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('the supply is named, not numbered', () => {
|
||||||
|
it('shows the name in the order list', async () => {
|
||||||
|
const wrapper = await render()
|
||||||
|
const name = wrapper.find('.order-list .supplyname')
|
||||||
|
expect(name.text()).toBe('414X Black')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows the name in the Part column', async () => {
|
||||||
|
const wrapper = await render()
|
||||||
|
const cells = wrapper.findAll('tbody .supplyname')
|
||||||
|
expect(cells.length).toBeGreaterThan(0)
|
||||||
|
expect(cells[0].text()).toBe('414X Black')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps the part number on the hover, both places', async () => {
|
||||||
|
const wrapper = await render()
|
||||||
|
for (const el of wrapper.findAll('.supplyname')) {
|
||||||
|
expect(el.attributes('title')).toBe('W2020X')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('never renders the part number as the visible label', async () => {
|
||||||
|
const wrapper = await render()
|
||||||
|
for (const el of wrapper.findAll('.supplyname')) {
|
||||||
|
expect(el.text()).not.toBe('W2020X')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('a supply with no name on file', () => {
|
||||||
|
it('falls back to the part number rather than showing nothing', async () => {
|
||||||
|
const wrapper = await render({ marketingname: null })
|
||||||
|
for (const el of wrapper.findAll('.supplyname')) {
|
||||||
|
expect(el.text()).toBe('W2020X')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('what purchasing receives is unchanged', () => {
|
||||||
|
it('copies the part number, not the name', async () => {
|
||||||
|
const written = []
|
||||||
|
Object.assign(navigator, {
|
||||||
|
clipboard: { writeText: (text) => { written.push(text); return Promise.resolve() } },
|
||||||
|
})
|
||||||
|
const wrapper = await render()
|
||||||
|
await wrapper.find('.order-head button').trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
expect(written).toHaveLength(1)
|
||||||
|
expect(written[0]).toContain('W2020X')
|
||||||
|
expect(written[0]).not.toContain('414X Black')
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user