Files
shopdb-flask/plugins/printers/frontend/views/PrinterDriversList.vue
cproudlock 2fb924f18e Let a printer driver carry the name Windows actually matches on
The driver form and the API both dropped `drivername` and `vendorid`. POST
returned 201 with both null, PUT accepted them and stored nothing, and no
screen offered either field - so there was no interface in the product that
could set them.

That is not a cosmetic gap. `drivername` is the exact string the INF declares,
and Add-PrinterDriver matches on it; `name` is ours to choose and means nothing
to Windows. With it null the shipped converger has nothing to match and
installs nothing, and the drift check that compares an observed driver name
against the expected one could never fire, because the expected value was never
stored. `vendorid` is how a universal driver (HP UPD, Xerox GPD) attaches to a
whole make instead of needing a near-duplicate row per model.

Both are now read on create and update, and the driver form gained the two
fields with hints saying what they are for - the INF name box says plainly that
leaving it blank means the converger cannot install that driver. A whitespace
value clears rather than stores: an empty box means "not known", and a blank
string would look set and match nothing.

Three tests. Verified by planting: drop the two fields from the create path
again and the round-trip test fails.

Scope: product
2026-09-03 15:16:33 -04:00

220 lines
8.3 KiB
Vue

<template>
<div>
<div class="page-header">
<h2>Printer Drivers</h2>
<label class="show-inactive"><input type="checkbox" v-model="showInactive" /> Show inactive</label>
<button class="btn btn-primary" @click="openModal()">+ Add Driver</button>
</div>
<div class="card">
<p class="hint">
Each driver is a name + a link to the driver package - an SMB path
(<code>\\server\share\driver</code>) or an HTTP URL. HTTP links open;
SMB paths are shown for copy-paste (browsers block file:// SMB).
</p>
<div v-if="loading" class="muted">Loading...</div>
<template v-else>
<div class="table-container">
<table>
<thead>
<tr>
<th>Name</th>
<th>Printer Model</th>
<th>Location</th>
<th>Description</th>
<th>Active</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="d in visibleItems" :key="d.driverid">
<td><strong>{{ d.name }}</strong></td>
<td>{{ d.modelname || '-' }}</td>
<td>
<a v-if="isHttp(d.location)" :href="d.location" target="_blank" class="mono">{{ d.location }}</a>
<span v-else class="mono">{{ d.location }}</span>
</td>
<td class="cell-truncate" :title="d.description">{{ d.description || '-' }}</td>
<td>
<span class="badge" :class="d.isactive ? 'badge-success' : 'badge-secondary'">{{ d.isactive ? 'yes' : 'no' }}</span>
</td>
<td class="actions">
<button class="btn btn-secondary btn-sm" @click="openModal(d)">Edit</button>
<button class="btn btn-danger btn-sm" @click="deleteDriver(d)">Delete</button>
</td>
</tr>
<tr v-if="visibleItems.length === 0">
<td colspan="6" style="text-align: center; color: var(--text-light);">No drivers</td>
</tr>
</tbody>
</table>
</div>
</template>
</div>
<div v-if="showModal" class="modal-overlay">
<div class="modal">
<div class="modal-header"><h3>{{ editing ? 'Edit' : 'Add' }} Driver</h3></div>
<form @submit.prevent="save">
<div class="modal-body">
<div class="form-group">
<label>Name *</label>
<input v-model="form.name" type="text" class="form-control" maxlength="150" required />
</div>
<div class="form-group">
<label>
INF Driver Name
<span class="hint">(the exact string the INF declares - what
Add-PrinterDriver matches on. Blank means the converger
cannot install this driver.)</span>
</label>
<input v-model="form.drivername" type="text" class="form-control"
maxlength="255"
placeholder="HP Universal Printing PCL 6" />
</div>
<div class="form-group">
<label>Vendor <span class="hint">(a universal driver serves every printer of that make; a model below wins over it)</span></label>
<select v-model="form.vendorid" class="form-control">
<option :value="null">-- none --</option>
<option v-for="v in vendors" :key="v.vendorid" :value="v.vendorid">
{{ v.vendor }}
</option>
</select>
</div>
<div class="form-group">
<label>Printer Model <span class="hint">(links driver to a model so it shows on matching printers)</span></label>
<select v-model="form.modelnumberid" class="form-control">
<option :value="null">-- none --</option>
<option v-for="m in models" :key="m.modelnumberid" :value="m.modelnumberid">
{{ m.modelnumber }}<template v-if="m.vendorname"> ({{ m.vendorname }})</template>
</option>
</select>
</div>
<div class="form-group">
<label>Location * <span class="hint">(SMB path or HTTP URL)</span></label>
<input v-model="form.location" type="text" class="form-control" maxlength="500"
placeholder="\\server\share\driver or https://..." required />
</div>
<div class="form-group">
<label>Description</label>
<textarea v-model="form.description" class="form-control" rows="2"></textarea>
</div>
<div class="form-group">
<label class="checkbox-label"><input type="checkbox" v-model="form.isactive" /> Active</label>
</div>
<div v-if="error" class="error-message">{{ error }}</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @click="closeModal">Cancel</button>
<button type="submit" class="btn btn-primary" :disabled="saving">{{ saving ? 'Saving...' : 'Save' }}</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { printersApi, vendorsApi } from '@/api'
import { useToast } from '@/composables/toast'
import { apiError } from '@/utils/apiError'
const toast = useToast()
const items = ref([])
const models = ref([])
const vendors = ref([])
const showInactive = ref(false)
const visibleItems = computed(() => showInactive.value ? items.value : items.value.filter(x => x.isactive !== false))
const loading = ref(true)
const showModal = ref(false)
const editing = ref(null)
const saving = ref(false)
const error = ref('')
const form = ref({ name: '', location: '', description: '', drivername: '', vendorid: null, modelnumberid: null, isactive: true })
function isHttp(loc) {
return typeof loc === 'string' && /^https?:\/\//i.test(loc)
}
onMounted(() => { loadData(); loadModels(); loadVendors() })
async function loadData() {
loading.value = true
try {
const response = await printersApi.drivers.list({ active: false })
items.value = response.data.data || []
} catch (err) {
console.error('Error loading drivers:', err)
} finally {
loading.value = false
}
}
async function loadModels() {
try {
const response = await printersApi.modelSupplies.listModels({ perpage: 100 })
models.value = response.data.data || []
} catch (err) {
console.error('Error loading models:', err)
}
}
async function loadVendors() {
try {
// A universal driver (HP UPD, Xerox GPD) attaches to the whole make rather
// than one model, which is what vendorid is for. listAll, because the
// picker must show every vendor - perpage is clamped at 100.
const response = await vendorsApi.list({ perpage: 100 })
vendors.value = response.data.data || []
} catch (err) {
console.error('Error loading vendors:', err)
}
}
function openModal(item = null) {
editing.value = item
form.value = item
? { name: item.name || '', location: item.location || '', description: item.description || '', drivername: item.drivername || '', vendorid: item.vendorid || null, modelnumberid: item.modelnumberid || null, isactive: item.isactive !== false }
: { name: '', location: '', description: '', drivername: '', vendorid: null, modelnumberid: null, isactive: true }
error.value = ''
showModal.value = true
}
function closeModal() { showModal.value = false; editing.value = null }
async function save() {
error.value = ''
saving.value = true
try {
if (editing.value) {
await printersApi.drivers.update(editing.value.driverid, form.value)
} else {
await printersApi.drivers.create(form.value)
}
closeModal()
loadData()
} catch (err) {
error.value = apiError(err, 'Failed to save')
} finally {
saving.value = false
}
}
async function deleteDriver(d) {
if (!confirm(`Delete driver "${d.name}"?`)) return
try {
await printersApi.drivers.delete(d.driverid)
loadData()
} catch (err) {
toast.error(apiError(err, 'Failed to delete'))
}
}
</script>
<style scoped>
.hint { color: var(--text-light); font-size: 0.9rem; margin: 0 0 14px; }
.muted { color: var(--text-light); }
.mono { font-family: monospace; font-size: 0.85rem; word-break: break-all; }
</style>