Operators reported losing a part-filled form by clicking slightly outside it. Every data-entry modal closed on a backdrop click with no warning and no way back - the worst possible response to a misplaced click, and it happens most to someone adding their first records at a new site. Close-on-overlay is removed from 35 modals across 30 files: anything containing an input, textarea, select or v-model. They still close by Cancel or the X. Confirmation dialogs keep it, because a delete prompt holds nothing to lose and dismissing one by clicking away is the behaviour people expect. VendorsList shows the distinction - its edit form no longer closes that way, its delete confirmation still does. The shared Modal component now defaults closeOnOverlay to FALSE. Every current caller holds a form, a checkout, a stock adjustment or a map position being picked, and not one passed the prop, so all of them had the same fault. A modal that genuinely wants dismissing that way opts in explicitly. Also regroups the operator console menu, which had grown to numbers 1-9 plus three letters bolted on with no order to them. Actions are now grouped by what they touch, keyed by their first letter, and the old numbers still work so nobody who has used it for months is stopped by a rearrangement. The menu also warns when the server is not fully provisioned and names the key that fixes it, instead of reporting it as ordinary status lines that read as normal unless you already knew what to look for. That check is cached for the session because it shells out to flask twice and the answer does not change while somebody reads the screen.
221 lines
6.6 KiB
Vue
221 lines
6.6 KiB
Vue
<template>
|
|
<div>
|
|
<div class="page-header">
|
|
<h1>PC Access Protocols</h1>
|
|
<div class="actions">
|
|
<router-link to="/settings" class="btn btn-secondary">Back to Settings</router-link>
|
|
<button class="btn btn-primary" @click="openNew">New Protocol</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<p class="hint">
|
|
Remote-access protocols offered on PCs. Links are built as
|
|
<code>{{ '{scheme}://{hostname}.<pc_access_domain>:{port}' }}</code> from
|
|
each protocol's template. Set the domain in
|
|
<router-link to="/settings/site">Site & Facility</router-link>.
|
|
</p>
|
|
<div class="table-container">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Scheme</th>
|
|
<th>Default port</th>
|
|
<th>Link template</th>
|
|
<th>Active</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="p in protocols" :key="p.protocolid">
|
|
<td><strong>{{ p.name }}</strong></td>
|
|
<td class="mono">{{ p.scheme }}</td>
|
|
<td>{{ p.defaultport ?? '-' }}</td>
|
|
<td class="mono">{{ p.linktemplate }}</td>
|
|
<td>
|
|
<span class="badge" :class="p.isactive ? 'badge-success' : 'badge-secondary'">
|
|
{{ p.isactive ? 'yes' : 'no' }}
|
|
</span>
|
|
</td>
|
|
<td class="actions">
|
|
<button class="btn btn-sm btn-secondary" @click="openEdit(p)">Edit</button>
|
|
<button class="btn btn-sm btn-danger" @click="remove(p)">Delete</button>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="!loading && !protocols.length">
|
|
<td colspan="6" class="muted" style="text-align:center;">No protocols.</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="editing" class="modal-overlay">
|
|
<div class="modal-panel">
|
|
<h2>{{ form.protocolid ? 'Edit' : 'New' }} Protocol</h2>
|
|
<div class="form-grid">
|
|
<label class="field">
|
|
<span>Name</span>
|
|
<input v-model="form.name" type="text" maxlength="50" placeholder="e.g. VNC" />
|
|
</label>
|
|
<label class="field">
|
|
<span>Scheme</span>
|
|
<input v-model="form.scheme" type="text" maxlength="20" placeholder="vnc / rdp / https / ssh" />
|
|
</label>
|
|
<label class="field">
|
|
<span>Default port</span>
|
|
<input v-model.number="form.defaultport" type="number" min="1" max="65535" placeholder="5900" />
|
|
</label>
|
|
<label class="field">
|
|
<span>Link template</span>
|
|
<input v-model="form.linktemplate" type="text" maxlength="255" placeholder="vnc://{host}:{port}" />
|
|
<small class="muted">Placeholders: <code>{host}</code>, <code>{port}</code>, <code>{scheme}</code></small>
|
|
</label>
|
|
<label class="field checkbox">
|
|
<input v-model="form.isactive" type="checkbox" />
|
|
<span>Active</span>
|
|
</label>
|
|
</div>
|
|
<p v-if="error" class="error">{{ error }}</p>
|
|
<div class="modal-actions">
|
|
<button class="btn btn-secondary" @click="close">Cancel</button>
|
|
<button class="btn btn-primary" :disabled="saving || !formValid" @click="save">
|
|
{{ saving ? 'Saving...' : 'Save' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { computersApi } from '@/api'
|
|
import { apiError } from '@/utils/apiError'
|
|
|
|
const protocols = ref([])
|
|
const loading = ref(true)
|
|
const editing = ref(false)
|
|
const saving = ref(false)
|
|
const error = ref('')
|
|
const form = ref({})
|
|
|
|
const formValid = computed(() =>
|
|
form.value.name && form.value.scheme && form.value.linktemplate
|
|
)
|
|
|
|
async function load() {
|
|
loading.value = true
|
|
try {
|
|
const response = await computersApi.protocols.list({ active: false })
|
|
protocols.value = response.data.data || []
|
|
} catch (err) {
|
|
console.error('Error loading protocols:', err)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function openNew() {
|
|
error.value = ''
|
|
form.value = { name: '', scheme: '', defaultport: null, linktemplate: '', isactive: true }
|
|
editing.value = true
|
|
}
|
|
|
|
function openEdit(p) {
|
|
error.value = ''
|
|
form.value = {
|
|
protocolid: p.protocolid,
|
|
name: p.name,
|
|
scheme: p.scheme,
|
|
defaultport: p.defaultport ?? null,
|
|
linktemplate: p.linktemplate,
|
|
isactive: p.isactive !== false
|
|
}
|
|
editing.value = true
|
|
}
|
|
|
|
function close() {
|
|
editing.value = false
|
|
}
|
|
|
|
async function save() {
|
|
saving.value = true
|
|
error.value = ''
|
|
try {
|
|
if (form.value.protocolid) {
|
|
await computersApi.protocols.update(form.value.protocolid, form.value)
|
|
} else {
|
|
await computersApi.protocols.create(form.value)
|
|
}
|
|
editing.value = false
|
|
await load()
|
|
} catch (err) {
|
|
error.value = apiError(err, 'Save failed.')
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
async function remove(p) {
|
|
if (!confirm(`Delete protocol "${p.name}"? (kept but deactivated if any PC uses it)`)) return
|
|
try {
|
|
await computersApi.protocols.remove(p.protocolid)
|
|
await load()
|
|
} catch (err) {
|
|
console.error('Error deleting protocol:', err)
|
|
}
|
|
}
|
|
|
|
onMounted(load)
|
|
</script>
|
|
|
|
<style scoped>
|
|
.hint {
|
|
color: var(--text-light);
|
|
font-size: 0.9rem;
|
|
margin: 0 0 14px;
|
|
}
|
|
.mono { font-family: monospace; font-size: 0.85rem; }
|
|
.muted { color: var(--text-light); }
|
|
|
|
.modal-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: center;
|
|
padding: 40px 16px;
|
|
overflow-y: auto;
|
|
z-index: 1000;
|
|
}
|
|
.modal-panel {
|
|
background: var(--bg-card);
|
|
color: var(--text);
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
padding: 24px;
|
|
width: 100%;
|
|
max-width: 520px;
|
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4);
|
|
}
|
|
.modal-panel h2 { margin: 0 0 18px; }
|
|
.form-grid { display: flex; flex-direction: column; gap: 14px; }
|
|
.field { display: flex; flex-direction: column; gap: 4px; }
|
|
.field > span { font-size: 0.85rem; color: var(--text-light); }
|
|
.field input[type="text"],
|
|
.field input[type="number"] {
|
|
padding: 8px 10px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
}
|
|
.field.checkbox { flex-direction: row; align-items: center; gap: 8px; }
|
|
.field.checkbox > span { color: var(--text); font-size: 1rem; }
|
|
.error { color: var(--danger); margin: 12px 0 0; }
|
|
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 22px; }
|
|
</style>
|