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
220 lines
8.3 KiB
Vue
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>
|