Stop a stray click outside a modal discarding what was typed
Some checks failed
CI / backend (push) Failing after 8s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 8s
CI / migrations-mysql (push) Failing after 7s

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.
This commit is contained in:
cproudlock
2026-08-05 13:42:20 -04:00
parent 23b1dfff41
commit d8fe0a48b2
32 changed files with 241 additions and 195 deletions

View File

@@ -98,7 +98,7 @@
</template>
<!-- Add Relationship Modal -->
<div v-if="showAddModal" class="modal-overlay" @click.self="closeModal">
<div v-if="showAddModal" class="modal-overlay">
<div class="modal-content">
<div class="modal-header">
<h3>Add Relationship</h3>

View File

@@ -1,145 +1,150 @@
<template>
<Teleport to="body">
<div v-if="modelValue" class="modal-overlay" @click.self="closeOnOverlay && close()">
<div class="modal-container" :class="sizeClass">
<div class="modal-header" v-if="title || $slots.header">
<slot name="header">
<h3>{{ title }}</h3>
</slot>
<button class="modal-close" @click="close" aria-label="Close">&times;</button>
</div>
<div class="modal-body">
<slot></slot>
</div>
<div class="modal-footer" v-if="$slots.footer">
<slot name="footer"></slot>
</div>
</div>
</div>
</Teleport>
</template>
<script setup>
import { computed, watch } from 'vue'
const props = defineProps({
modelValue: { type: Boolean, default: false },
title: { type: String, default: '' },
size: { type: String, default: 'medium' }, // small, medium, large, fullscreen
closeOnOverlay: { type: Boolean, default: true }
})
const emit = defineEmits(['update:modelValue', 'close'])
const sizeClass = computed(() => `modal-${props.size}`)
function close() {
emit('update:modelValue', false)
emit('close')
}
// Handle escape key
watch(() => props.modelValue, (isOpen) => {
if (isOpen) {
document.addEventListener('keydown', handleEscape)
document.body.style.overflow = 'hidden'
} else {
document.removeEventListener('keydown', handleEscape)
document.body.style.overflow = ''
}
})
function handleEscape(e) {
if (e.key === 'Escape') close()
}
</script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1rem;
}
.modal-container {
background: var(--bg-card-solid);
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
max-height: 90vh;
overflow: hidden;
color: var(--text);
}
.modal-small {
width: 400px;
max-width: 90vw;
}
.modal-medium {
width: 600px;
max-width: 90vw;
}
.modal-large {
width: 900px;
max-width: 95vw;
}
.modal-fullscreen {
width: 95vw;
height: 90vh;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border);
}
.modal-header h3 {
margin: 0;
font-size: 1.25rem;
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--text-light);
padding: 0;
line-height: 1;
}
.modal-close:hover {
color: var(--text);
}
.modal-body {
flex: 1;
overflow: auto;
padding: 1.5rem;
}
.modal-footer {
padding: 1rem 1.5rem;
border-top: 1px solid var(--border);
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
</style>
<template>
<Teleport to="body">
<div v-if="modelValue" class="modal-overlay" @click.self="closeOnOverlay && close()">
<div class="modal-container" :class="sizeClass">
<div class="modal-header" v-if="title || $slots.header">
<slot name="header">
<h3>{{ title }}</h3>
</slot>
<button class="modal-close" @click="close" aria-label="Close">&times;</button>
</div>
<div class="modal-body">
<slot></slot>
</div>
<div class="modal-footer" v-if="$slots.footer">
<slot name="footer"></slot>
</div>
</div>
</div>
</Teleport>
</template>
<script setup>
import { computed, watch } from 'vue'
const props = defineProps({
modelValue: { type: Boolean, default: false },
title: { type: String, default: '' },
size: { type: String, default: 'medium' }, // small, medium, large, fullscreen
// Defaults to FALSE. Every current user of this component holds either a
// form, a checkout, a stock adjustment or a map position being picked, and a
// stray click on the backdrop threw all of it away without asking - which is
// what operators complained about. A modal that genuinely wants dismissing
// that way can still opt in with :close-on-overlay="true".
closeOnOverlay: { type: Boolean, default: false }
})
const emit = defineEmits(['update:modelValue', 'close'])
const sizeClass = computed(() => `modal-${props.size}`)
function close() {
emit('update:modelValue', false)
emit('close')
}
// Handle escape key
watch(() => props.modelValue, (isOpen) => {
if (isOpen) {
document.addEventListener('keydown', handleEscape)
document.body.style.overflow = 'hidden'
} else {
document.removeEventListener('keydown', handleEscape)
document.body.style.overflow = ''
}
})
function handleEscape(e) {
if (e.key === 'Escape') close()
}
</script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1rem;
}
.modal-container {
background: var(--bg-card-solid);
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
max-height: 90vh;
overflow: hidden;
color: var(--text);
}
.modal-small {
width: 400px;
max-width: 90vw;
}
.modal-medium {
width: 600px;
max-width: 90vw;
}
.modal-large {
width: 900px;
max-width: 95vw;
}
.modal-fullscreen {
width: 95vw;
height: 90vh;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border);
}
.modal-header h3 {
margin: 0;
font-size: 1.25rem;
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--text-light);
padding: 0;
line-height: 1;
}
.modal-close:hover {
color: var(--text);
}
.modal-body {
flex: 1;
overflow: auto;
padding: 1.5rem;
}
.modal-footer {
padding: 1rem 1.5rem;
border-top: 1px solid var(--border);
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
</style>

View File

@@ -109,7 +109,7 @@
</div>
<!-- Create modal -->
<div v-if="showCreate" class="modal-overlay" @click.self="closeCreate">
<div v-if="showCreate" class="modal-overlay">
<div class="modal modal-lg">
<div class="modal-header"><h3>New API Token</h3></div>
<form @submit.prevent="createToken">
@@ -174,7 +174,7 @@
</div>
<!-- Edit modal (scopes) -->
<div v-if="editing" class="modal-overlay" @click.self="closeEdit">
<div v-if="editing" class="modal-overlay">
<div class="modal modal-lg">
<div class="modal-header"><h3>Edit token access</h3></div>
<form @submit.prevent="saveEdit">

View File

@@ -38,7 +38,7 @@
</template>
</div>
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
<div v-if="showModal" class="modal-overlay">
<div class="modal">
<div class="modal-header"><h3>Edit {{ form.assettype }}</h3></div>
<form @submit.prevent="save">

View File

@@ -50,7 +50,7 @@
</div>
<!-- Add/Edit Modal -->
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
<div v-if="showModal" class="modal-overlay">
<div class="modal">
<div class="modal-header">
<h3>{{ editing ? 'Edit Business Unit' : 'Add Business Unit' }}</h3>

View File

@@ -67,7 +67,7 @@
</template>
</div>
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
<div v-if="showModal" class="modal-overlay">
<div class="modal">
<div class="modal-header"><h3>{{ editing ? 'Edit' : 'Add' }} Field</h3></div>
<form @submit.prevent="save">

View File

@@ -49,7 +49,7 @@
</div>
<!-- Add/Edit Modal -->
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
<div v-if="showModal" class="modal-overlay">
<div class="modal">
<div class="modal-header">
<h3>{{ editing ? 'Edit Mapping' : 'Add Mapping' }}</h3>

View File

@@ -39,7 +39,7 @@
</template>
</div>
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
<div v-if="showModal" class="modal-overlay">
<div class="modal">
<div class="modal-header"><h3>{{ editing ? 'Edit' : 'Add' }} Location Type</h3></div>
<form @submit.prevent="save">

View File

@@ -77,7 +77,7 @@
</div>
<!-- Add/Edit Modal -->
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
<div v-if="showModal" class="modal-overlay">
<div class="modal">
<div class="modal-header">
<h3>{{ editingLocation ? 'Edit Location' : 'Add Location' }}</h3>

View File

@@ -64,7 +64,7 @@
</div>
<!-- Add/Edit Modal -->
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
<div v-if="showModal" class="modal-overlay">
<div class="modal">
<div class="modal-header">
<h3>{{ editingType ? 'Edit Model Type' : 'Add Model Type' }}</h3>

View File

@@ -77,7 +77,7 @@
</div>
<!-- Add/Edit Modal -->
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
<div v-if="showModal" class="modal-overlay">
<div class="modal modal-lg">
<div class="modal-header">
<h3>{{ editingModel ? 'Edit Model' : 'Add Model' }}</h3>

View File

@@ -41,7 +41,7 @@
</template>
</div>
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
<div v-if="showModal" class="modal-overlay">
<div class="modal">
<div class="modal-header"><h3>{{ editing ? 'Edit' : 'Add' }} Relationship Type</h3></div>
<form @submit.prevent="save">

View File

@@ -67,7 +67,7 @@
</div>
<!-- Add/Edit Modal -->
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
<div v-if="showModal" class="modal-overlay">
<div class="modal">
<div class="modal-header">
<h3>{{ editingStatus ? 'Edit Status' : 'Add Status' }}</h3>

View File

@@ -48,7 +48,7 @@
</div>
<!-- Team Add/Edit Modal -->
<div v-if="showTeamModal" class="modal-overlay" @click.self="showTeamModal = false">
<div v-if="showTeamModal" class="modal-overlay">
<div class="modal">
<div class="modal-header">
<h3>{{ editingTeam ? 'Edit Support Team' : 'Add Support Team' }}</h3>
@@ -130,7 +130,7 @@
</div>
<!-- Contact Add/Edit Modal -->
<div v-if="showContactModal" class="modal-overlay" @click.self="showContactModal = false">
<div v-if="showContactModal" class="modal-overlay">
<div class="modal">
<div class="modal-header">
<h3>{{ editingContact ? 'Edit Contact' : 'Add Contact' }}</h3>

View File

@@ -112,7 +112,7 @@
</div>
<!-- User Modal -->
<div v-if="showCreateModal || editingUser" class="modal-overlay" @click.self="closeUserModal">
<div v-if="showCreateModal || editingUser" class="modal-overlay">
<div class="modal">
<div class="modal-header">
<h3>{{ editingUser ? 'Edit User' : 'Create User' }}</h3>
@@ -190,7 +190,7 @@
</div>
<!-- Role Modal -->
<div v-if="showRoleModal || editingRole" class="modal-overlay" @click.self="closeRoleModal">
<div v-if="showRoleModal || editingRole" class="modal-overlay">
<div class="modal modal-lg">
<div class="modal-header">
<h3>{{ editingRole ? 'Edit Role' : 'Create Role' }}</h3>

View File

@@ -73,7 +73,7 @@
</div>
<!-- Add/Edit Modal -->
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
<div v-if="showModal" class="modal-overlay">
<div class="modal">
<div class="modal-header">
<h3>{{ editingVendor ? 'Edit Vendor' : 'Add Vendor' }}</h3>