dashboard: printer rows are a name and its cartridges, with the answer on hover
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

The printer card was a name followed by a comma-joined string of cartridges and
a location - the widest row on the board, and the one running past the card
edge.

Now: the printer name links to its page and reveals its location on hover, and
each depleted cartridge is its own chip showing "Black 4%", revealing the part
number to order on hover. The percentage says something is wrong; the part
number says what to do about it, which today means opening the printer's page
to find out. Every capacity tier is listed, as the report has always done.

Two additions to the card contract, both general: 'chips' maps a row key to a
list of {text, title, level}, and 'titletooltip' puts context on the row title.
Nothing load-bearing goes in a tooltip - hover is not discoverable and does not
exist on touch - so a chip always states the fact and only explains it on hover.

Chips are bordered rather than filled: a row of solid red pills reads as an
emergency even when a cartridge is merely low.

Also repaired a self-inflicted mess. A string-slice edit used a marker that
appears EARLIER in the file, so the slice was empty and two helpers were
injected at line 1, above the module docstring. Removed; the file parses and
the helpers live beside the route they serve.
This commit is contained in:
cproudlock
2026-08-11 15:46:21 -04:00
parent c34815b87e
commit 8623db3ee2
5 changed files with 119 additions and 16 deletions

View File

@@ -18,11 +18,17 @@
<!-- exceptions / list: one line per thing, each linking to itself -->
<ul v-else class="dc-rows">
<li v-for="(row, index) in visibleRows(card)" :key="index" class="dc-row">
<router-link v-if="row.link" :to="row.link" class="dc-row-title">
<router-link v-if="row.link" :to="row.link" class="dc-row-title"
:title="row.titletip || undefined">
{{ row.title }}
</router-link>
<span v-else class="dc-row-title dc-row-nolink">{{ row.title }}</span>
<span v-else class="dc-row-title dc-row-nolink"
:title="row.titletip || undefined">{{ row.title }}</span>
<span v-if="row.detail" class="dc-row-detail">{{ row.detail }}</span>
<span v-for="(chip, c) in row.chips" :key="c" class="dc-chip"
:class="'dc-chip-' + (chip.level || 'low')" :title="chip.title">
{{ chip.text }}
</span>
<span v-if="row.meta.length" class="dc-row-meta">
{{ row.meta.map((m) => m.text).join(' / ') }}
</span>
@@ -204,6 +210,21 @@ defineExpose({ load })
overflow: hidden;
text-overflow: ellipsis;
}
/* One chip per depleted supply. Bordered rather than filled: a row of solid
red pills reads as an emergency even when a cartridge is merely low. */
.dc-chip {
flex: none;
padding: 0.05rem 0.4rem;
border: 1px solid var(--border);
border-radius: 10px;
font-size: 0.75rem;
color: var(--text);
white-space: nowrap;
cursor: help;
}
.dc-chip-critical { border-color: var(--danger); color: var(--danger); }
.dc-chip-low { border-color: var(--warning); }
.dc-more {
display: block;
margin: 0.5rem 0 0;

View File

@@ -89,11 +89,34 @@ export function overflowCount(card) {
return Math.max(0, rows(card).length - MAXROWS)
}
// Chips are the row's own small facts, each carrying a tooltip: a depleted
// cartridge shows "Black 4%" and reveals the part number to order on hover.
// The percentage says something is wrong; the tooltip says what to do about
// it, which otherwise means opening the printer's page to find out.
export function mapChips(card, item) {
const key = card.map && card.map.chips
const list = key ? item[key] : null
if (!Array.isArray(list)) return []
return list
.filter((chip) => chip && chip.text)
.map((chip) => ({ text: chip.text, title: chip.title || '', level: chip.level || '' }))
}
// A tooltip on the row title, for context that would clutter the line - a
// printer's location, say. Never load-bearing: hover is not discoverable and
// does not exist on touch, so nothing essential goes here.
export function mapTitleTip(card, item) {
const key = card.map && card.map.titletooltip
return key ? (item[key] || '') : ''
}
export function cardRows(card) {
return rows(card).map((item) => ({
title: mapTitle(card, item),
titletip: mapTitleTip(card, item),
detail: mapDetail(card, item),
meta: mapMeta(card, item),
chips: mapChips(card, item),
link: mapLink(card, item),
timestamp: card.map && card.map.timestamp ? item[card.map.timestamp] : null,
}))

View File

@@ -163,3 +163,42 @@ describe('gating', () => {
expect(kept.map((c) => c.id)).toEqual(['ok'])
})
})
describe('chips and tooltips', () => {
const printerCard = {
render: 'exceptions',
map: {
title: 'printername',
titletooltip: 'location',
chips: 'supplies',
link: '/printers/{printerid}',
},
_data: [{
printerid: 7,
printername: 'WJ-HP-402',
location: 'Cell B, north wall',
supplies: [
{ text: 'Black 4%', title: 'CF226X (high)', level: 'critical' },
{ text: 'Cyan 9%', title: 'No part number on file for this model', level: 'low' },
],
}],
}
it('builds one chip per depleted supply, each with its reorder tooltip', () => {
const [row] = cardRows(printerCard)
expect(row.chips.map((c) => c.text)).toEqual(['Black 4%', 'Cyan 9%'])
expect(row.chips[0].title).toBe('CF226X (high)')
expect(row.chips[0].level).toBe('critical')
})
it('puts the location on the title as a tooltip, not in the line', () => {
const [row] = cardRows(printerCard)
expect(row.titletip).toBe('Cell B, north wall')
expect(row.detail).toBe('')
})
it('has no chips when a card declares none', () => {
expect(cardRows({ render: 'exceptions', map: { title: 'x' }, _data: [{ x: 'y' }] })[0].chips)
.toEqual([])
})
})