Printer card chips
This commit is contained in:
@@ -18,11 +18,17 @@
|
|||||||
<!-- exceptions / list: one line per thing, each linking to itself -->
|
<!-- exceptions / list: one line per thing, each linking to itself -->
|
||||||
<ul v-else class="dc-rows">
|
<ul v-else class="dc-rows">
|
||||||
<li v-for="(row, index) in visibleRows(card)" :key="index" class="dc-row">
|
<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 }}
|
{{ row.title }}
|
||||||
</router-link>
|
</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-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">
|
<span v-if="row.meta.length" class="dc-row-meta">
|
||||||
{{ row.meta.map((m) => m.text).join(' / ') }}
|
{{ row.meta.map((m) => m.text).join(' / ') }}
|
||||||
</span>
|
</span>
|
||||||
@@ -204,6 +210,21 @@ defineExpose({ load })
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
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 {
|
.dc-more {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0.5rem 0 0;
|
margin: 0.5rem 0 0;
|
||||||
|
|||||||
@@ -89,11 +89,34 @@ export function overflowCount(card) {
|
|||||||
return Math.max(0, rows(card).length - MAXROWS)
|
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) {
|
export function cardRows(card) {
|
||||||
return rows(card).map((item) => ({
|
return rows(card).map((item) => ({
|
||||||
title: mapTitle(card, item),
|
title: mapTitle(card, item),
|
||||||
|
titletip: mapTitleTip(card, item),
|
||||||
detail: mapDetail(card, item),
|
detail: mapDetail(card, item),
|
||||||
meta: mapMeta(card, item),
|
meta: mapMeta(card, item),
|
||||||
|
chips: mapChips(card, item),
|
||||||
link: mapLink(card, item),
|
link: mapLink(card, item),
|
||||||
timestamp: card.map && card.map.timestamp ? item[card.map.timestamp] : null,
|
timestamp: card.map && card.map.timestamp ? item[card.map.timestamp] : null,
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -163,3 +163,42 @@ describe('gating', () => {
|
|||||||
expect(kept.map((c) => c.id)).toEqual(['ok'])
|
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([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -1407,6 +1407,24 @@ def _shortsupplyname(name):
|
|||||||
return text or 'supply'
|
return text or 'supply'
|
||||||
|
|
||||||
|
|
||||||
|
def _reordertip(supply):
|
||||||
|
"""What to order, for the chip's tooltip.
|
||||||
|
|
||||||
|
A percentage says a cartridge is nearly out; the part number says what to
|
||||||
|
buy, which is the next thing someone needs and today means opening the
|
||||||
|
printer's page to find it. Every capacity tier is listed, because the
|
||||||
|
report has always shown all reorder options.
|
||||||
|
"""
|
||||||
|
parts = supply.get('partnumbers') or []
|
||||||
|
if not parts:
|
||||||
|
return 'No part number on file for this model'
|
||||||
|
return ', '.join(
|
||||||
|
'{}{}'.format(part['partnumber'],
|
||||||
|
' ({})'.format(part['capacitytier'])
|
||||||
|
if part.get('capacitytier') else '')
|
||||||
|
for part in parts)
|
||||||
|
|
||||||
|
|
||||||
@printers_asset_bp.route('/dashboard/supplies', methods=['GET'])
|
@printers_asset_bp.route('/dashboard/supplies', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
@require_permission('printers.view')
|
@require_permission('printers.view')
|
||||||
@@ -1429,22 +1447,21 @@ def dashboard_supplies():
|
|||||||
lows = [s for s in printer['supplies'] if s['status'] == 'low']
|
lows = [s for s in printer['supplies'] if s['status'] == 'low']
|
||||||
if not criticals and not lows:
|
if not criticals and not lows:
|
||||||
continue
|
continue
|
||||||
# Every depleted supply, criticals first. Listing only the worst class
|
# Criticals first. Every depleted supply is listed, not just the worst
|
||||||
# would hide a low cartridge behind a critical one on the same printer,
|
# class - whoever walks out there wants to carry both cartridges.
|
||||||
# and whoever walks out there wants to carry both.
|
|
||||||
worst = criticals + lows
|
|
||||||
rows.append({
|
rows.append({
|
||||||
'printerid': printer['printerid'],
|
'printerid': printer['printerid'],
|
||||||
'printername': printer['printername'] or printer['assetnumber'],
|
'printername': printer['printername'] or printer['assetnumber'],
|
||||||
'location': printer['location'],
|
'location': printer['location'] or 'No location set',
|
||||||
'status': 'critical' if criticals else 'low',
|
|
||||||
# 'remaining' is the percent left. There is no 'percent' key -
|
|
||||||
# reading one rendered every cartridge as "None%" on the board.
|
|
||||||
'supplies': ', '.join(
|
|
||||||
'{} {}%'.format(_shortsupplyname(s.get('name')),
|
|
||||||
s.get('remaining'))
|
|
||||||
for s in worst),
|
|
||||||
'iscritical': bool(criticals),
|
'iscritical': bool(criticals),
|
||||||
|
# One chip per depleted supply: what is low, and on hover what to
|
||||||
|
# order to fix it.
|
||||||
|
'supplies': [{
|
||||||
|
'text': '{} {}%'.format(_shortsupplyname(supply.get('name')),
|
||||||
|
supply.get('remaining')),
|
||||||
|
'title': _reordertip(supply),
|
||||||
|
'level': supply.get('status'),
|
||||||
|
} for supply in criticals + lows],
|
||||||
})
|
})
|
||||||
|
|
||||||
rows.sort(key=lambda r: (not r['iscritical'], r['printername']))
|
rows.sort(key=lambda r: (not r['iscritical'], r['printername']))
|
||||||
|
|||||||
@@ -306,8 +306,11 @@ class PrintersPlugin(BasePlugin):
|
|||||||
'position': 40,
|
'position': 40,
|
||||||
'map': {
|
'map': {
|
||||||
'title': 'printername',
|
'title': 'printername',
|
||||||
'detail': 'supplies',
|
# Location on hover rather than on the line: it is context
|
||||||
'meta': [{'key': 'location'}, {'key': 'status'}],
|
# for "where do I walk", not part of the finding, and it
|
||||||
|
# was the text pushing rows past the card edge.
|
||||||
|
'titletooltip': 'location',
|
||||||
|
'chips': 'supplies',
|
||||||
'link': '/printers/{printerid}',
|
'link': '/printers/{printerid}',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user