Improve user creation UX with modal dialog

- Replace redirect flow with AJAX + modal popup
- Show credentials side-by-side (username | password)
- Compact warning message and right-aligned action buttons
- Add Another resets form, Done returns to user list
- Narrow flash messages to match card width

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Aaron D. Lee
2026-01-04 00:10:48 -05:00
parent 823b8824ea
commit 8e5f01754f
3 changed files with 124 additions and 12 deletions

View File

@@ -1552,9 +1552,17 @@ def admin_user_new():
password = request.form.get("password", "") password = request.form.get("password", "")
success, message, user = create_user(username, password) success, message, user = create_user(username, password)
# Check if AJAX request
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
if success:
return jsonify({"success": True, "username": username, "password": password})
else:
return jsonify({"success": False, "error": message})
# Regular form submission fallback
if success: if success:
flash(f"User '{username}' created successfully", "success") flash(f"User '{username}' created successfully", "success")
# Store password temporarily for display
session["temp_password"] = password session["temp_password"] = password
session["temp_username"] = username session["temp_username"] = username
return redirect(url_for("admin_user_created")) return redirect(url_for("admin_user_created"))

View File

@@ -11,12 +11,12 @@
<span class="fs-5">Add New User</span> <span class="fs-5">Add New User</span>
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="POST" action="{{ url_for('admin_user_new') }}"> <form id="createUserForm">
<div class="mb-3"> <div class="mb-3">
<label class="form-label"> <label class="form-label">
<i class="bi bi-person me-1"></i> Username <i class="bi bi-person me-1"></i> Username
</label> </label>
<input type="text" name="username" class="form-control" <input type="text" name="username" id="usernameInput" class="form-control"
placeholder="e.g., john_doe or john@example.com" placeholder="e.g., john_doe or john@example.com"
pattern="[a-zA-Z0-9][a-zA-Z0-9_\-@.]{2,79}" pattern="[a-zA-Z0-9][a-zA-Z0-9_\-@.]{2,79}"
title="3-80 characters, letters/numbers/underscore/hyphen/@/." title="3-80 characters, letters/numbers/underscore/hyphen/@/."
@@ -44,8 +44,10 @@
</div> </div>
</div> </div>
<div id="errorAlert" class="alert alert-danger d-none"></div>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<button type="submit" class="btn btn-primary flex-grow-1"> <button type="submit" class="btn btn-primary flex-grow-1" id="createBtn">
<i class="bi bi-person-check me-2"></i>Create User <i class="bi bi-person-check me-2"></i>Create User
</button> </button>
<a href="{{ url_for('admin_users') }}" class="btn btn-outline-secondary"> <a href="{{ url_for('admin_users') }}" class="btn btn-outline-secondary">
@@ -57,8 +59,108 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Success Modal -->
<div class="modal fade" id="successModal" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-success">
<div class="modal-header bg-success text-white">
<h5 class="modal-title">
<i class="bi bi-check-circle me-2"></i>User Created
</h5>
</div>
<div class="modal-body">
<div class="alert alert-warning mb-3 py-2">
<i class="bi bi-exclamation-triangle me-1"></i>
Password shown once. Copy it now.
</div>
<div class="row mb-3">
<div class="col-6">
<label class="form-label text-muted small mb-1">Username</label>
<div class="input-group">
<input type="text" class="form-control font-monospace"
id="createdUsername" readonly>
<button class="btn btn-outline-secondary" type="button"
onclick="copyField('createdUsername')" title="Copy">
<i class="bi bi-clipboard"></i>
</button>
</div>
</div>
<div class="col-6">
<label class="form-label text-muted small mb-1">Password</label>
<div class="input-group">
<input type="text" class="form-control font-monospace"
id="createdPassword" readonly>
<button class="btn btn-outline-secondary" type="button"
onclick="copyField('createdPassword')" title="Copy">
<i class="bi bi-clipboard"></i>
</button>
</div>
</div>
</div>
<div class="d-flex justify-content-end gap-2">
<button type="button" class="btn btn-primary" onclick="addAnother()">
<i class="bi bi-person-plus me-1"></i>Add Another
</button>
<a href="{{ url_for('admin_users') }}" class="btn btn-outline-secondary">
Done
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="{{ url_for('static', filename='js/auth.js') }}"></script> <script src="{{ url_for('static', filename='js/auth.js') }}"></script>
<script>
const form = document.getElementById('createUserForm');
const errorAlert = document.getElementById('errorAlert');
const createBtn = document.getElementById('createBtn');
const successModal = new bootstrap.Modal(document.getElementById('successModal'));
form.addEventListener('submit', async function(e) {
e.preventDefault();
errorAlert.classList.add('d-none');
createBtn.disabled = true;
createBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Creating...';
const formData = new FormData(form);
try {
const response = await fetch('{{ url_for("admin_user_new") }}', {
method: 'POST',
body: formData,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await response.json();
if (data.success) {
document.getElementById('createdUsername').value = data.username;
document.getElementById('createdPassword').value = data.password;
successModal.show();
} else {
errorAlert.textContent = data.error;
errorAlert.classList.remove('d-none');
}
} catch (err) {
errorAlert.textContent = 'An error occurred. Please try again.';
errorAlert.classList.remove('d-none');
}
createBtn.disabled = false;
createBtn.innerHTML = '<i class="bi bi-person-check me-2"></i>Create User';
});
function addAnother() {
successModal.hide();
document.getElementById('usernameInput').value = '';
regeneratePassword();
document.getElementById('usernameInput').focus();
}
</script>
{% endblock %} {% endblock %}

View File

@@ -67,6 +67,7 @@
<main class="container py-5"> <main class="container py-5">
{% with messages = get_flashed_messages(with_categories=true) %} {% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %} {% if messages %}
<div class="mx-auto mb-3" style="max-width: 460px;">
{% for category, message in messages %} {% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else ('warning' if category == 'warning' else 'success') }} alert-dismissible fade show" role="alert"> <div class="alert alert-{{ 'danger' if category == 'error' else ('warning' if category == 'warning' else 'success') }} alert-dismissible fade show" role="alert">
<i class="bi bi-{{ 'exclamation-triangle' if category == 'error' else ('exclamation-circle' if category == 'warning' else 'check-circle') }} me-2"></i> <i class="bi bi-{{ 'exclamation-triangle' if category == 'error' else ('exclamation-circle' if category == 'warning' else 'check-circle') }} me-2"></i>
@@ -74,6 +75,7 @@
<button type="button" class="btn-close" data-bs-dismiss="alert"></button> <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div> </div>
{% endfor %} {% endfor %}
</div>
{% endif %} {% endif %}
{% endwith %} {% endwith %}