mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-09-20 05:20:53 +02:00
feat: Enhance user management and role deletion functionality
- Added toast notifications for user actions, improving user feedback on operations such as creation, deletion, and updates. - Implemented a confirmation dialog for user deletions, ensuring users are prompted before irreversible actions. - Refactored user management templates to streamline the user interface and enhance usability. - Improved error handling for user operations, providing clearer messages for success and failure scenarios. - Updated JavaScript functions to manage user interactions and modal displays more effectively.
This commit is contained in:
+209
-53
@@ -19,6 +19,9 @@ type RolesData struct {
|
||||
|
||||
templ AdminRoles(ctx context.Context, data RolesData) {
|
||||
@LayoutWithContext("Role Management", ctx) {
|
||||
<!-- Toast container for status messages -->
|
||||
<div id="toast-container" class="fixed top-5 right-5 z-50 flex flex-col gap-2"></div>
|
||||
|
||||
<!-- Roles Management Content -->
|
||||
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||
@@ -52,6 +55,18 @@ templ AdminRoles(ctx context.Context, data RolesData) {
|
||||
</tr>
|
||||
} else {
|
||||
for _, role := range data.Roles {
|
||||
<!-- Dialog for this role -->
|
||||
@RoleDialog(
|
||||
fmt.Sprintf("delete-role-dialog-%d", role.ID),
|
||||
"Delete Role",
|
||||
fmt.Sprintf("Are you sure you want to delete role \"%s\"? This action cannot be undone.", role.Name),
|
||||
"bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700",
|
||||
"Delete Role",
|
||||
"delete",
|
||||
role.ID,
|
||||
role.Name,
|
||||
)
|
||||
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<td class="px-6 py-4 text-sm text-gray-900 dark:text-white">{ role.Name }</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{ role.Description }</td>
|
||||
@@ -72,8 +87,7 @@ templ AdminRoles(ctx context.Context, data RolesData) {
|
||||
<button
|
||||
data-role-id={ fmt.Sprint(role.ID) }
|
||||
data-role-name={ role.Name }
|
||||
data-modal-target="confirm-delete-modal"
|
||||
data-modal-toggle="confirm-delete-modal"
|
||||
onclick={ showRoleDialog(fmt.Sprintf("delete-role-dialog-%d", role.ID)) }
|
||||
class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
@@ -87,51 +101,134 @@ templ AdminRoles(ctx context.Context, data RolesData) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div id="confirm-delete-modal" tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
|
||||
<div class="relative p-4 w-full max-w-md max-h-full">
|
||||
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||
<div class="p-4 md:p-5 text-center">
|
||||
<i class="fas fa-exclamation-triangle mx-auto mb-4 text-red-400 text-3xl"></i>
|
||||
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">Are you sure you want to delete this role?</h3>
|
||||
<div>
|
||||
<button
|
||||
id="confirm-delete-button"
|
||||
type="button"
|
||||
class="text-white bg-red-600 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 dark:focus:ring-red-800 font-medium rounded-lg text-sm inline-flex items-center px-5 py-2.5 text-center mr-2">
|
||||
Yes, delete it
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-modal-hide="confirm-delete-modal"
|
||||
class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
|
||||
No, cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
let currentRoleId = null;
|
||||
const modal = document.getElementById('confirm-delete-modal');
|
||||
// Function to create and show a toast
|
||||
function showToast(message, type) {
|
||||
const toastContainer = document.getElementById('toast-container');
|
||||
|
||||
// Handle delete modal
|
||||
const deleteButtons = document.querySelectorAll('[data-modal-target="confirm-delete-modal"]');
|
||||
deleteButtons.forEach(button => {
|
||||
// Create toast element
|
||||
const toast = document.createElement('div');
|
||||
toast.id = 'toast-' + type + '-' + Date.now();
|
||||
toast.className = 'flex items-center w-full max-w-xs p-4 mb-4 rounded-lg shadow text-gray-500 bg-white dark:text-gray-400 dark:bg-gray-800 transform translate-y-16 opacity-0 transition-all duration-300 ease-out';
|
||||
toast.role = 'alert';
|
||||
|
||||
// Set toast content based on type
|
||||
let iconClass, bgColorClass, textColorClass;
|
||||
|
||||
if (type === 'success') {
|
||||
iconClass = 'text-green-500 bg-green-100 dark:bg-green-800 dark:text-green-200';
|
||||
bgColorClass = 'text-green-500 dark:text-green-200';
|
||||
textColorClass = 'text-green-500 dark:text-green-200';
|
||||
} else if (type === 'error') {
|
||||
iconClass = 'text-red-500 bg-red-100 dark:bg-red-800 dark:text-red-200';
|
||||
bgColorClass = 'text-red-500 dark:text-red-200';
|
||||
textColorClass = 'text-red-500 dark:text-red-200';
|
||||
} else {
|
||||
iconClass = 'text-blue-500 bg-blue-100 dark:bg-blue-800 dark:text-blue-200';
|
||||
bgColorClass = 'text-blue-500 dark:text-blue-200';
|
||||
textColorClass = 'text-blue-500 dark:text-blue-200';
|
||||
}
|
||||
|
||||
// Set inner HTML with appropriate icon and message
|
||||
toast.innerHTML = `
|
||||
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 rounded-lg ${iconClass}">
|
||||
${type === 'success'
|
||||
? '<i class="fas fa-check"></i>'
|
||||
: type === 'error'
|
||||
? '<i class="fas fa-exclamation-circle"></i>'
|
||||
: '<i class="fas fa-info-circle"></i>'}
|
||||
</div>
|
||||
<div class="ml-3 text-sm font-normal">${message}</div>
|
||||
<button type="button" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" data-dismiss-target="#${toast.id}" aria-label="Close">
|
||||
<span class="sr-only">Close</span>
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
// Add toast to container
|
||||
toastContainer.appendChild(toast);
|
||||
|
||||
// Trigger animation after a small delay to ensure the DOM has updated
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('translate-y-16', 'opacity-0');
|
||||
toast.classList.add('translate-y-0', 'opacity-100');
|
||||
}, 10);
|
||||
|
||||
// Add event listener to close button
|
||||
const closeButton = toast.querySelector('button[data-dismiss-target]');
|
||||
closeButton.addEventListener('click', function() {
|
||||
// Animate out before removing
|
||||
toast.classList.add('opacity-0', 'translate-y-4');
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Auto-remove toast after 5 seconds
|
||||
setTimeout(() => {
|
||||
toast.classList.add('opacity-0', 'translate-y-4');
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Handle modal hide buttons
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const hideButtons = document.querySelectorAll('[data-modal-hide]');
|
||||
hideButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
currentRoleId = this.getAttribute('data-role-id');
|
||||
const modalId = this.getAttribute('data-modal-hide');
|
||||
const modal = document.getElementById(modalId);
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
});
|
||||
});
|
||||
|
||||
// Handle confirm delete button
|
||||
const confirmDeleteButton = document.getElementById('confirm-delete-button');
|
||||
confirmDeleteButton.addEventListener('click', function() {
|
||||
if (currentRoleId) {
|
||||
// Use fetch to send DELETE request
|
||||
fetch(`/admin/roles/${currentRoleId}`, {
|
||||
// Check for URL parameters after page load to show success/error messages
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const successMsg = urlParams.get('success');
|
||||
const errorMsg = urlParams.get('error');
|
||||
|
||||
// Show success toast if parameter exists
|
||||
if (successMsg) {
|
||||
showToast(decodeURIComponent(successMsg), 'success');
|
||||
}
|
||||
|
||||
// Show error toast if parameter exists
|
||||
if (errorMsg) {
|
||||
showToast(decodeURIComponent(errorMsg), 'error');
|
||||
}
|
||||
|
||||
// Show error from the data if it exists
|
||||
if (document.querySelector('[data-error-message]')) {
|
||||
const errorMsg = document.querySelector('[data-error-message]').getAttribute('data-error-message');
|
||||
if (errorMsg) {
|
||||
showToast(errorMsg, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Set up window variables to track deletions
|
||||
window.lastDeletedRole = null;
|
||||
window.isRoleDeletingRequest = false;
|
||||
});
|
||||
|
||||
// Handle role deletion
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const confirmDeleteButtons = document.querySelectorAll('[id^="delete-role-btn-"]');
|
||||
confirmDeleteButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const roleId = this.getAttribute('data-role-id');
|
||||
const roleName = this.getAttribute('data-role-name');
|
||||
|
||||
// Store deletion info for event tracking
|
||||
window.lastDeletedRole = {
|
||||
id: roleId,
|
||||
name: roleName
|
||||
};
|
||||
|
||||
// Send DELETE request
|
||||
fetch(`/admin/roles/${roleId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -139,29 +236,88 @@ templ AdminRoles(ctx context.Context, data RolesData) {
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
// Reload page on success
|
||||
window.location.reload();
|
||||
// Show success toast
|
||||
showToast(`Role "${roleName}" deleted successfully`, 'success');
|
||||
// Reload page on success after a short delay
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
// Handle error
|
||||
response.json().then(data => {
|
||||
alert('Error: ' + (data.error || 'Failed to delete role'));
|
||||
return response.json().then(data => {
|
||||
showToast(`Error: ${data.error || 'Failed to delete role'}`, 'error');
|
||||
}).catch(() => {
|
||||
alert('An error occurred while deleting the role');
|
||||
showToast(`Error deleting role "${roleName}"`, 'error');
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('An error occurred while deleting the role');
|
||||
})
|
||||
.finally(() => {
|
||||
// Hide modal
|
||||
const closeModalButton = document.querySelector('[data-modal-hide="confirm-delete-modal"]');
|
||||
if (closeModalButton) closeModalButton.click();
|
||||
showToast(`Error: ${error.message || 'Failed to connect to server'}`, 'error');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Store error message if it exists -->
|
||||
if data.Error != "" {
|
||||
<div data-error-message={ data.Error } class="hidden"></div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RoleDialog for confirmation actions
|
||||
templ RoleDialog(id string, title string, message string, confirmClass string, confirmText string, action string, roleID uint, roleName string) {
|
||||
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm">
|
||||
<div class="relative p-4 w-full max-w-md max-h-full mx-auto">
|
||||
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||
<div class="p-6 text-center">
|
||||
if action == "delete" {
|
||||
<i class="fas fa-trash-alt text-red-400 text-3xl mb-4"></i>
|
||||
} else {
|
||||
<i class="fas fa-exclamation-triangle text-yellow-400 text-3xl mb-4"></i>
|
||||
}
|
||||
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">{ message }</h3>
|
||||
<button
|
||||
type="button"
|
||||
class="text-white font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 bg-red-600 hover:bg-red-700 focus:ring-4 focus:outline-none focus:ring-red-300 dark:bg-red-500 dark:hover:bg-red-600 dark:focus:ring-red-800"
|
||||
id={ fmt.Sprintf("delete-role-btn-%d", roleID) }
|
||||
data-role-id={ fmt.Sprint(roleID) }
|
||||
data-role-name={ roleName }
|
||||
onclick={ triggerRoleDelete(id, roleID, roleName) }>
|
||||
{ confirmText }
|
||||
</button>
|
||||
<button type="button" data-modal-hide={ id } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
script hideRoleDialog(id string) {
|
||||
document.getElementById(id).classList.add("hidden");
|
||||
document.getElementById(id).classList.remove("flex");
|
||||
}
|
||||
|
||||
script showRoleDialog(id string) {
|
||||
document.getElementById(id).classList.remove("hidden");
|
||||
document.getElementById(id).classList.add("flex");
|
||||
}
|
||||
|
||||
script triggerRoleDelete(dialogId string, roleID uint, roleName string) {
|
||||
// Hide the dialog
|
||||
document.getElementById(dialogId).classList.add("hidden");
|
||||
document.getElementById(dialogId).classList.remove("flex");
|
||||
|
||||
// Store data in a way that's accessible to event handlers
|
||||
window.lastDeletedRole = {
|
||||
id: roleID,
|
||||
name: roleName
|
||||
};
|
||||
|
||||
// Add custom marker to track this deletion
|
||||
window.isRoleDeletingRequest = true;
|
||||
}
|
||||
+95
-35
@@ -12,7 +12,11 @@ templ ConfigDialog(id string, title string, message string, confirmClass string,
|
||||
<div class="relative p-4 w-full max-w-md max-h-full mx-auto">
|
||||
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||
<div class="p-6 text-center">
|
||||
<i class="fas fa-exclamation-circle text-yellow-400 fa-3x flex-shrink-0 mx-auto mb-4"></i>
|
||||
if action == "delete" {
|
||||
<i class="fas fa-trash-alt text-red-400 text-3xl mb-4"></i>
|
||||
} else {
|
||||
<i class="fas fa-exclamation-circle text-yellow-400 text-3xl mb-4"></i>
|
||||
}
|
||||
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">{ message }</h3>
|
||||
<button
|
||||
type="button"
|
||||
@@ -26,7 +30,7 @@ templ ConfigDialog(id string, title string, message string, confirmClass string,
|
||||
onclick={ triggerConfigDelete(id, configID, configName) }>
|
||||
{ confirmText }
|
||||
</button>
|
||||
<button type="button" class="py-2.5 px-5 ms-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700" onclick={ hideConfigDialog(id) }>
|
||||
<button type="button" data-modal-hide={ id } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
@@ -37,6 +41,7 @@ templ ConfigDialog(id string, title string, message string, confirmClass string,
|
||||
|
||||
script hideConfigDialog(id string) {
|
||||
document.getElementById(id).classList.add("hidden");
|
||||
document.getElementById(id).classList.remove("flex");
|
||||
}
|
||||
|
||||
script showConfigDialog(id string) {
|
||||
@@ -68,38 +73,82 @@ type ConfigsData struct {
|
||||
|
||||
templ Configs(ctx context.Context, data ConfigsData) {
|
||||
@LayoutWithContext("Transfer Configurations", ctx) {
|
||||
<!-- Status and Error Messages -->
|
||||
<div id="toast-container" class="fixed top-5 right-5 z-50 flex flex-col gap-2"></div>
|
||||
|
||||
<script>
|
||||
// Debug notification system
|
||||
console.log("Configs template loaded, setting up notification system");
|
||||
|
||||
// Create a global notyf instance if it doesn't exist yet
|
||||
if (!window.notyf) {
|
||||
window.notyf = new Notyf({
|
||||
duration: 3000,
|
||||
position: {
|
||||
x: 'right',
|
||||
y: 'top',
|
||||
},
|
||||
types: [
|
||||
{
|
||||
type: 'success',
|
||||
background: '#38c172',
|
||||
icon: {
|
||||
className: 'fas fa-check-circle',
|
||||
tagName: 'i'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'error',
|
||||
background: '#e3342f',
|
||||
icon: {
|
||||
className: 'fas fa-exclamation-circle',
|
||||
tagName: 'i'
|
||||
}
|
||||
}
|
||||
]
|
||||
// Function to create and show a toast
|
||||
function showToast(message, type) {
|
||||
const toastContainer = document.getElementById('toast-container');
|
||||
|
||||
// Create toast element
|
||||
const toast = document.createElement('div');
|
||||
toast.id = 'toast-' + type + '-' + Date.now();
|
||||
toast.className = 'flex items-center w-full max-w-xs p-4 mb-4 rounded-lg shadow text-gray-500 bg-white dark:text-gray-400 dark:bg-gray-800 transform translate-y-16 opacity-0 transition-all duration-300 ease-out';
|
||||
toast.role = 'alert';
|
||||
|
||||
// Set toast content based on type
|
||||
let iconClass, bgColorClass, textColorClass;
|
||||
|
||||
if (type === 'success') {
|
||||
iconClass = 'text-green-500 bg-green-100 dark:bg-green-800 dark:text-green-200';
|
||||
bgColorClass = 'text-green-500 dark:text-green-200';
|
||||
textColorClass = 'text-green-500 dark:text-green-200';
|
||||
} else if (type === 'error') {
|
||||
iconClass = 'text-red-500 bg-red-100 dark:bg-red-800 dark:text-red-200';
|
||||
bgColorClass = 'text-red-500 dark:text-red-200';
|
||||
textColorClass = 'text-red-500 dark:text-red-200';
|
||||
} else {
|
||||
iconClass = 'text-blue-500 bg-blue-100 dark:bg-blue-800 dark:text-blue-200';
|
||||
bgColorClass = 'text-blue-500 dark:text-blue-200';
|
||||
textColorClass = 'text-blue-500 dark:text-blue-200';
|
||||
}
|
||||
|
||||
// Set inner HTML with appropriate icon and message
|
||||
toast.innerHTML = `
|
||||
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 rounded-lg ${iconClass}">
|
||||
${type === 'success'
|
||||
? '<i class="fas fa-check"></i>'
|
||||
: type === 'error'
|
||||
? '<i class="fas fa-exclamation-circle"></i>'
|
||||
: '<i class="fas fa-info-circle"></i>'}
|
||||
</div>
|
||||
<div class="ml-3 text-sm font-normal">${message}</div>
|
||||
<button type="button" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" data-dismiss-target="#${toast.id}" aria-label="Close">
|
||||
<span class="sr-only">Close</span>
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
// Add toast to container
|
||||
toastContainer.appendChild(toast);
|
||||
|
||||
// Trigger animation after a small delay to ensure the DOM has updated
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('translate-y-16', 'opacity-0');
|
||||
toast.classList.add('translate-y-0', 'opacity-100');
|
||||
}, 10);
|
||||
|
||||
// Add event listener to close button
|
||||
const closeButton = toast.querySelector('button[data-dismiss-target]');
|
||||
closeButton.addEventListener('click', function() {
|
||||
// Animate out before removing
|
||||
toast.classList.add('opacity-0', 'translate-y-4');
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
});
|
||||
console.log("Notyf initialized:", window.notyf);
|
||||
|
||||
// Auto-remove toast after 5 seconds
|
||||
setTimeout(() => {
|
||||
toast.classList.add('opacity-0', 'translate-y-4');
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Show status messages based on URL parameters
|
||||
@@ -115,12 +164,23 @@ templ Configs(ctx context.Context, data ConfigsData) {
|
||||
if (errorDetails) {
|
||||
message += ": " + errorDetails;
|
||||
}
|
||||
window.notyf.error(message);
|
||||
showToast(message, 'error');
|
||||
}
|
||||
|
||||
if (status === 'gdrive_auth_success') {
|
||||
window.notyf.success("Google Drive authentication completed successfully");
|
||||
showToast("Google Drive authentication completed successfully", 'success');
|
||||
}
|
||||
|
||||
// Handle modal hide buttons
|
||||
const hideButtons = document.querySelectorAll('[data-modal-hide]');
|
||||
hideButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const modalId = this.getAttribute('data-modal-hide');
|
||||
const modal = document.getElementById(modalId);
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Track all HTMX events for debugging
|
||||
@@ -180,7 +240,7 @@ templ Configs(ctx context.Context, data ConfigsData) {
|
||||
configName = window.lastDeletedConfig.name;
|
||||
}
|
||||
|
||||
window.notyf.success(`Configuration "${configName}" deleted successfully`);
|
||||
showToast(`Configuration "${configName}" deleted successfully`, 'success');
|
||||
|
||||
// Clear flags
|
||||
window.currentlyDeletingConfig = false;
|
||||
@@ -197,7 +257,7 @@ templ Configs(ctx context.Context, data ConfigsData) {
|
||||
// If this is a successful duplication request, show notification
|
||||
if (isDuplicateRequest && event.detail.successful) {
|
||||
const configName = window.duplicatingConfigName || "configuration";
|
||||
window.notyf.success(`Configuration "${configName}" duplicated successfully`);
|
||||
showToast(`Configuration "${configName}" duplicated successfully`, 'success');
|
||||
|
||||
// Clear flags
|
||||
window.isConfigDuplicateRequest = false;
|
||||
@@ -239,7 +299,7 @@ templ Configs(ctx context.Context, data ConfigsData) {
|
||||
errorMsg = `Error: ${error.error}`;
|
||||
}
|
||||
|
||||
window.notyf.error(errorMsg);
|
||||
showToast(errorMsg, 'error');
|
||||
|
||||
// Clear flags
|
||||
window.currentlyDeletingConfig = false;
|
||||
@@ -270,7 +330,7 @@ templ Configs(ctx context.Context, data ConfigsData) {
|
||||
}
|
||||
}
|
||||
|
||||
window.notyf.error(errorMsg);
|
||||
showToast(errorMsg, 'error');
|
||||
|
||||
// Clear flags
|
||||
window.isConfigDuplicateRequest = false;
|
||||
|
||||
+552
-78
@@ -76,87 +76,43 @@ func formatFileSize(size int64) string {
|
||||
|
||||
// Updated Dialog component using Flowbite
|
||||
templ FileMetadataDialog(id string, title string, message string, confirmClass string, confirmText string, action string, fileID uint, fileName string, section string) {
|
||||
<!-- Modal -->
|
||||
<div id={id} tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
|
||||
<div class="relative p-4 w-full max-w-md max-h-full">
|
||||
<!-- Modal content -->
|
||||
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm">
|
||||
<div class="relative p-4 w-full max-w-md max-h-full mx-auto">
|
||||
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||
<!-- Modal header -->
|
||||
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
{ title }
|
||||
</h3>
|
||||
<button type="button" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white" onclick={ hideFileDialog(id) }>
|
||||
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
|
||||
</svg>
|
||||
<span class="sr-only">Close modal</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Modal body -->
|
||||
<div class="p-4 md:p-5 space-y-4">
|
||||
<p class="text-base leading-relaxed text-gray-500 dark:text-gray-400">
|
||||
{ message }
|
||||
</p>
|
||||
</div>
|
||||
<!-- Modal footer -->
|
||||
<div class="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600">
|
||||
if section == "list" {
|
||||
if confirmClass == "btn-danger" {
|
||||
<button
|
||||
type="button"
|
||||
class="text-white font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 bg-red-600 hover:bg-red-700 focus:ring-4 focus:outline-none focus:ring-red-300 dark:bg-red-500 dark:hover:bg-red-600 dark:focus:ring-red-800"
|
||||
hx-delete={ fmt.Sprintf("/files/%d", fileID) }
|
||||
hx-target={ fmt.Sprintf("#file-row-%d", fileID) }
|
||||
hx-swap="delete"
|
||||
data-file-name={ fileName }
|
||||
data-file-id={ fmt.Sprint(fileID) }
|
||||
id={ fmt.Sprintf("delete-file-btn-%d", fileID) }
|
||||
onclick={ triggerFileDelete(id, fileID, fileName) }>
|
||||
{ confirmText }
|
||||
</button>
|
||||
} else {
|
||||
<button
|
||||
type="button"
|
||||
class="text-white font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
|
||||
hx-delete={ fmt.Sprintf("/files/%d", fileID) }
|
||||
hx-target={ fmt.Sprintf("#file-row-%d", fileID) }
|
||||
hx-swap="delete"
|
||||
data-file-name={ fileName }
|
||||
data-file-id={ fmt.Sprint(fileID) }
|
||||
id={ fmt.Sprintf("delete-file-btn-%d", fileID) }
|
||||
onclick={ triggerFileDelete(id, fileID, fileName) }>
|
||||
{ confirmText }
|
||||
</button>
|
||||
}
|
||||
<div class="p-6 text-center">
|
||||
if action == "delete" {
|
||||
<i class="fas fa-trash-alt text-red-400 text-3xl mb-4"></i>
|
||||
} else {
|
||||
if confirmClass == "btn-danger" {
|
||||
<button
|
||||
type="button"
|
||||
class="text-white font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 bg-red-600 hover:bg-red-700 focus:ring-4 focus:outline-none focus:ring-red-300 dark:bg-red-500 dark:hover:bg-red-600 dark:focus:ring-red-800"
|
||||
hx-delete={ fmt.Sprintf("/files/%d", fileID) }
|
||||
hx-redirect="/files"
|
||||
data-file-name={ fileName }
|
||||
data-file-id={ fmt.Sprint(fileID) }
|
||||
id={ fmt.Sprintf("delete-file-btn-%d", fileID) }
|
||||
onclick={ triggerFileDelete(id, fileID, fileName) }>
|
||||
{ confirmText }
|
||||
</button>
|
||||
} else {
|
||||
<button
|
||||
type="button"
|
||||
class="text-white font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
|
||||
hx-delete={ fmt.Sprintf("/files/%d", fileID) }
|
||||
hx-redirect="/files"
|
||||
data-file-name={ fileName }
|
||||
data-file-id={ fmt.Sprint(fileID) }
|
||||
id={ fmt.Sprintf("delete-file-btn-%d", fileID) }
|
||||
onclick={ triggerFileDelete(id, fileID, fileName) }>
|
||||
{ confirmText }
|
||||
</button>
|
||||
}
|
||||
<i class="fas fa-exclamation-triangle text-yellow-400 text-3xl mb-4"></i>
|
||||
}
|
||||
<button type="button" class="py-2.5 px-5 ms-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700" onclick={ hideFileDialog(id) }>
|
||||
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">{ message }</h3>
|
||||
if section == "list" {
|
||||
<button
|
||||
type="button"
|
||||
class="text-white font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 bg-red-600 hover:bg-red-700 focus:ring-4 focus:outline-none focus:ring-red-300 dark:bg-red-500 dark:hover:bg-red-600 dark:focus:ring-red-800"
|
||||
hx-delete={ fmt.Sprintf("/files/%d", fileID) }
|
||||
hx-target={ fmt.Sprintf("#file-row-%d", fileID) }
|
||||
hx-swap="delete"
|
||||
data-file-name={ fileName }
|
||||
data-file-id={ fmt.Sprint(fileID) }
|
||||
id={ fmt.Sprintf("delete-file-btn-%d", fileID) }
|
||||
onclick={ triggerFileDelete(id, fileID, fileName) }>
|
||||
{ confirmText }
|
||||
</button>
|
||||
} else {
|
||||
<button
|
||||
type="button"
|
||||
class="text-white font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 bg-red-600 hover:bg-red-700 focus:ring-4 focus:outline-none focus:ring-red-300 dark:bg-red-500 dark:hover:bg-red-600 dark:focus:ring-red-800"
|
||||
hx-delete={ fmt.Sprintf("/files/%d", fileID) }
|
||||
hx-redirect="/files"
|
||||
data-file-name={ fileName }
|
||||
data-file-id={ fmt.Sprint(fileID) }
|
||||
id={ fmt.Sprintf("delete-file-btn-%d", fileID) }
|
||||
onclick={ triggerFileDelete(id, fileID, fileName) }>
|
||||
{ confirmText }
|
||||
</button>
|
||||
}
|
||||
<button type="button" data-modal-hide={ id } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
@@ -167,15 +123,18 @@ templ FileMetadataDialog(id string, title string, message string, confirmClass s
|
||||
|
||||
script hideFileDialog(id string) {
|
||||
document.getElementById(id).classList.add("hidden");
|
||||
document.getElementById(id).classList.remove("flex");
|
||||
}
|
||||
|
||||
script showFileDialog(id string) {
|
||||
document.getElementById(id).classList.remove("hidden");
|
||||
document.getElementById(id).classList.add("flex");
|
||||
}
|
||||
|
||||
script triggerFileDelete(dialogId string, fileID uint, fileName string) {
|
||||
// Hide the dialog
|
||||
document.getElementById(dialogId).classList.add("hidden");
|
||||
document.getElementById(dialogId).classList.remove("flex");
|
||||
|
||||
// Store data in a way that's accessible to event handlers
|
||||
window.lastDeletedFile = {
|
||||
@@ -341,6 +300,203 @@ templ FileMetadataListPartial(data FileMetadataListData) {
|
||||
// Update the main FileMetadataList template to use the partial template
|
||||
templ FileMetadataList(ctx context.Context, data FileMetadataListData) {
|
||||
@LayoutWithContext("File Metadata", ctx) {
|
||||
<!-- Status and Error Messages -->
|
||||
<div id="toast-container" class="fixed top-5 right-5 z-50 flex flex-col gap-2"></div>
|
||||
|
||||
<script>
|
||||
// Debug notification system
|
||||
console.log("File Metadata template loaded, setting up notification system");
|
||||
|
||||
// Function to create and show a toast
|
||||
function showToast(message, type) {
|
||||
const toastContainer = document.getElementById('toast-container');
|
||||
|
||||
// Create toast element
|
||||
const toast = document.createElement('div');
|
||||
toast.id = 'toast-' + type + '-' + Date.now();
|
||||
toast.className = 'flex items-center w-full max-w-xs p-4 mb-4 rounded-lg shadow text-gray-500 bg-white dark:text-gray-400 dark:bg-gray-800 transform translate-y-16 opacity-0 transition-all duration-300 ease-out';
|
||||
toast.role = 'alert';
|
||||
|
||||
// Set toast content based on type
|
||||
let iconClass, bgColorClass, textColorClass;
|
||||
|
||||
if (type === 'success') {
|
||||
iconClass = 'text-green-500 bg-green-100 dark:bg-green-800 dark:text-green-200';
|
||||
bgColorClass = 'text-green-500 dark:text-green-200';
|
||||
textColorClass = 'text-green-500 dark:text-green-200';
|
||||
} else if (type === 'error') {
|
||||
iconClass = 'text-red-500 bg-red-100 dark:bg-red-800 dark:text-red-200';
|
||||
bgColorClass = 'text-red-500 dark:text-red-200';
|
||||
textColorClass = 'text-red-500 dark:text-red-200';
|
||||
} else {
|
||||
iconClass = 'text-blue-500 bg-blue-100 dark:bg-blue-800 dark:text-blue-200';
|
||||
bgColorClass = 'text-blue-500 dark:text-blue-200';
|
||||
textColorClass = 'text-blue-500 dark:text-blue-200';
|
||||
}
|
||||
|
||||
// Set inner HTML with appropriate icon and message
|
||||
toast.innerHTML = `
|
||||
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 rounded-lg ${iconClass}">
|
||||
${type === 'success'
|
||||
? '<i class="fas fa-check"></i>'
|
||||
: type === 'error'
|
||||
? '<i class="fas fa-exclamation-circle"></i>'
|
||||
: '<i class="fas fa-info-circle"></i>'}
|
||||
</div>
|
||||
<div class="ml-3 text-sm font-normal">${message}</div>
|
||||
<button type="button" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" data-dismiss-target="#${toast.id}" aria-label="Close">
|
||||
<span class="sr-only">Close</span>
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
// Add toast to container
|
||||
toastContainer.appendChild(toast);
|
||||
|
||||
// Trigger animation after a small delay to ensure the DOM has updated
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('translate-y-16', 'opacity-0');
|
||||
toast.classList.add('translate-y-0', 'opacity-100');
|
||||
}, 10);
|
||||
|
||||
// Add event listener to close button
|
||||
const closeButton = toast.querySelector('button[data-dismiss-target]');
|
||||
closeButton.addEventListener('click', function() {
|
||||
// Animate out before removing
|
||||
toast.classList.add('opacity-0', 'translate-y-4');
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Auto-remove toast after 5 seconds
|
||||
setTimeout(() => {
|
||||
toast.classList.add('opacity-0', 'translate-y-4');
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Handle modal hide buttons
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const hideButtons = document.querySelectorAll('[data-modal-hide]');
|
||||
hideButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const modalId = this.getAttribute('data-modal-hide');
|
||||
const modal = document.getElementById(modalId);
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Track all HTMX events for debugging
|
||||
document.addEventListener('htmx:beforeRequest', function(event) {
|
||||
console.log("HTMX before request:", event.detail);
|
||||
|
||||
// Check if this is a DELETE request by examining the URL and method
|
||||
const path = event.detail.path;
|
||||
const method = event.detail.verb;
|
||||
|
||||
console.log(`Request path: ${path}, method: ${method}`);
|
||||
|
||||
// Pattern match for file deletions (e.g., /files/123)
|
||||
if (path && method === 'DELETE' && path.match(/^\/files\/\d+$/)) {
|
||||
console.log("Detected file deletion request via URL pattern");
|
||||
|
||||
// This is definitely a delete request - store this information
|
||||
window.isFileDeleteRequest = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Track HTMX after-request events for file deletion
|
||||
document.addEventListener('htmx:afterRequest', function(event) {
|
||||
console.log("HTMX after request:", event.detail);
|
||||
|
||||
// Check for file deletion multiple ways
|
||||
const isDeleteRequest =
|
||||
// Check global flag from the triggerFileDelete function
|
||||
window.currentlyDeletingFile ||
|
||||
// Check flag from beforeRequest handler
|
||||
window.isFileDeleteRequest ||
|
||||
// Check URL pattern directly from this event
|
||||
(event.detail.pathInfo && event.detail.pathInfo.requestPath &&
|
||||
event.detail.pathInfo.requestPath.match(/^\/files\/\d+$/) &&
|
||||
event.detail.verb === 'DELETE');
|
||||
|
||||
console.log(`Is delete request: ${isDeleteRequest}`);
|
||||
|
||||
// If this is a successful delete request, show notification
|
||||
if (isDeleteRequest && event.detail.successful) {
|
||||
console.log("Delete request was successful");
|
||||
|
||||
let fileName = "Unknown";
|
||||
|
||||
// Try multiple sources for file name
|
||||
if (event.detail.elt && event.detail.elt.getAttribute) {
|
||||
fileName = event.detail.elt.getAttribute('data-file-name') || fileName;
|
||||
}
|
||||
|
||||
if (fileName === "Unknown" && window.lastDeletedFile) {
|
||||
// Fallback to our stored file info
|
||||
fileName = window.lastDeletedFile.name;
|
||||
}
|
||||
|
||||
console.log(`Showing success notification for deleted file: ${fileName}`);
|
||||
showToast(`File "${fileName}" deleted successfully`, 'success');
|
||||
|
||||
// Clear flags
|
||||
window.currentlyDeletingFile = false;
|
||||
window.isFileDeleteRequest = false;
|
||||
window.lastDeletedFile = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Track HTMX error events for file deletion
|
||||
document.addEventListener('htmx:responseError', function(event) {
|
||||
console.log("HTMX response error:", event.detail);
|
||||
|
||||
// Similar logic as success but for errors
|
||||
const isDeleteRequest =
|
||||
window.currentlyDeletingFile ||
|
||||
window.isFileDeleteRequest ||
|
||||
(event.detail.pathInfo && event.detail.pathInfo.requestPath &&
|
||||
event.detail.pathInfo.requestPath.match(/^\/files\/\d+$/) &&
|
||||
event.detail.verb === 'DELETE');
|
||||
|
||||
if (isDeleteRequest) {
|
||||
console.log("Delete request failed");
|
||||
|
||||
let fileName = "Unknown";
|
||||
|
||||
// Try multiple sources for file name
|
||||
if (event.detail.elt && event.detail.elt.getAttribute) {
|
||||
fileName = event.detail.elt.getAttribute('data-file-name') || fileName;
|
||||
}
|
||||
|
||||
if (fileName === "Unknown" && window.lastDeletedFile) {
|
||||
// Fallback to our stored file info
|
||||
fileName = window.lastDeletedFile.name;
|
||||
}
|
||||
|
||||
let errorMsg = `Failed to delete file "${fileName}"`;
|
||||
|
||||
if (event.detail.xhr && event.detail.xhr.responseText) {
|
||||
errorMsg = `Error: ${event.detail.xhr.responseText}`;
|
||||
}
|
||||
|
||||
console.log(`Showing error notification: ${errorMsg}`);
|
||||
showToast(errorMsg, 'error');
|
||||
|
||||
// Clear flags
|
||||
window.currentlyDeletingFile = false;
|
||||
window.isFileDeleteRequest = false;
|
||||
window.lastDeletedFile = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div id="file-metadata-container" style="min-height: 100vh;" class="bg-gray-50 dark:bg-gray-900">
|
||||
<div class="pb-8 w-full">
|
||||
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
@@ -450,6 +606,165 @@ templ FileMetadataList(ctx context.Context, data FileMetadataListData) {
|
||||
// FileMetadataDetails renders detailed information about a file
|
||||
templ FileMetadataDetails(ctx context.Context, data FileMetadataDetailsData) {
|
||||
@LayoutWithContext("File Details", ctx) {
|
||||
<!-- Status and Error Messages -->
|
||||
<div id="toast-container" class="fixed top-5 right-5 z-50 flex flex-col gap-2"></div>
|
||||
|
||||
<script>
|
||||
// Debug notification system
|
||||
console.log("File Details template loaded, setting up notification system");
|
||||
|
||||
// Function to create and show a toast
|
||||
function showToast(message, type) {
|
||||
const toastContainer = document.getElementById('toast-container');
|
||||
|
||||
// Create toast element
|
||||
const toast = document.createElement('div');
|
||||
toast.id = 'toast-' + type + '-' + Date.now();
|
||||
toast.className = 'flex items-center w-full max-w-xs p-4 mb-4 rounded-lg shadow text-gray-500 bg-white dark:text-gray-400 dark:bg-gray-800 transform translate-y-16 opacity-0 transition-all duration-300 ease-out';
|
||||
toast.role = 'alert';
|
||||
|
||||
// Set toast content based on type
|
||||
let iconClass, bgColorClass, textColorClass;
|
||||
|
||||
if (type === 'success') {
|
||||
iconClass = 'text-green-500 bg-green-100 dark:bg-green-800 dark:text-green-200';
|
||||
bgColorClass = 'text-green-500 dark:text-green-200';
|
||||
textColorClass = 'text-green-500 dark:text-green-200';
|
||||
} else if (type === 'error') {
|
||||
iconClass = 'text-red-500 bg-red-100 dark:bg-red-800 dark:text-red-200';
|
||||
bgColorClass = 'text-red-500 dark:text-red-200';
|
||||
textColorClass = 'text-red-500 dark:text-red-200';
|
||||
} else {
|
||||
iconClass = 'text-blue-500 bg-blue-100 dark:bg-blue-800 dark:text-blue-200';
|
||||
bgColorClass = 'text-blue-500 dark:text-blue-200';
|
||||
textColorClass = 'text-blue-500 dark:text-blue-200';
|
||||
}
|
||||
|
||||
// Set inner HTML with appropriate icon and message
|
||||
toast.innerHTML = `
|
||||
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 rounded-lg ${iconClass}">
|
||||
${type === 'success'
|
||||
? '<i class="fas fa-check"></i>'
|
||||
: type === 'error'
|
||||
? '<i class="fas fa-exclamation-circle"></i>'
|
||||
: '<i class="fas fa-info-circle"></i>'}
|
||||
</div>
|
||||
<div class="ml-3 text-sm font-normal">${message}</div>
|
||||
<button type="button" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" data-dismiss-target="#${toast.id}" aria-label="Close">
|
||||
<span class="sr-only">Close</span>
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
// Add toast to container
|
||||
toastContainer.appendChild(toast);
|
||||
|
||||
// Trigger animation after a small delay to ensure the DOM has updated
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('translate-y-16', 'opacity-0');
|
||||
toast.classList.add('translate-y-0', 'opacity-100');
|
||||
}, 10);
|
||||
|
||||
// Add event listener to close button
|
||||
const closeButton = toast.querySelector('button[data-dismiss-target]');
|
||||
closeButton.addEventListener('click', function() {
|
||||
// Animate out before removing
|
||||
toast.classList.add('opacity-0', 'translate-y-4');
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Auto-remove toast after 5 seconds
|
||||
setTimeout(() => {
|
||||
toast.classList.add('opacity-0', 'translate-y-4');
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Handle modal hide buttons
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const hideButtons = document.querySelectorAll('[data-modal-hide]');
|
||||
hideButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const modalId = this.getAttribute('data-modal-hide');
|
||||
const modal = document.getElementById(modalId);
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Track deletion events
|
||||
document.addEventListener('htmx:beforeRequest', function(event) {
|
||||
if (event.detail.path && event.detail.path.match(/^\/files\/\d+$/) && event.detail.verb === 'DELETE') {
|
||||
window.isFileDeleteRequest = true;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('htmx:afterRequest', function(event) {
|
||||
const isDeleteRequest =
|
||||
window.currentlyDeletingFile ||
|
||||
window.isFileDeleteRequest ||
|
||||
(event.detail.pathInfo && event.detail.pathInfo.requestPath &&
|
||||
event.detail.pathInfo.requestPath.match(/^\/files\/\d+$/) &&
|
||||
event.detail.verb === 'DELETE');
|
||||
|
||||
if (isDeleteRequest && event.detail.successful) {
|
||||
let fileName = "Unknown";
|
||||
|
||||
if (event.detail.elt && event.detail.elt.getAttribute) {
|
||||
fileName = event.detail.elt.getAttribute('data-file-name') || fileName;
|
||||
}
|
||||
|
||||
if (fileName === "Unknown" && window.lastDeletedFile) {
|
||||
fileName = window.lastDeletedFile.name;
|
||||
}
|
||||
|
||||
showToast(`File "${fileName}" deleted successfully`, 'success');
|
||||
|
||||
window.currentlyDeletingFile = false;
|
||||
window.isFileDeleteRequest = false;
|
||||
window.lastDeletedFile = null;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('htmx:responseError', function(event) {
|
||||
const isDeleteRequest =
|
||||
window.currentlyDeletingFile ||
|
||||
window.isFileDeleteRequest ||
|
||||
(event.detail.pathInfo && event.detail.pathInfo.requestPath &&
|
||||
event.detail.pathInfo.requestPath.match(/^\/files\/\d+$/) &&
|
||||
event.detail.verb === 'DELETE');
|
||||
|
||||
if (isDeleteRequest) {
|
||||
let fileName = "Unknown";
|
||||
|
||||
if (event.detail.elt && event.detail.elt.getAttribute) {
|
||||
fileName = event.detail.elt.getAttribute('data-file-name') || fileName;
|
||||
}
|
||||
|
||||
if (fileName === "Unknown" && window.lastDeletedFile) {
|
||||
fileName = window.lastDeletedFile.name;
|
||||
}
|
||||
|
||||
let errorMsg = `Failed to delete file "${fileName}"`;
|
||||
|
||||
if (event.detail.xhr && event.detail.xhr.responseText) {
|
||||
errorMsg = `Error: ${event.detail.xhr.responseText}`;
|
||||
}
|
||||
|
||||
showToast(errorMsg, 'error');
|
||||
|
||||
window.currentlyDeletingFile = false;
|
||||
window.isFileDeleteRequest = false;
|
||||
window.lastDeletedFile = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div id="file-details-container" style="min-height: 100vh;" class="bg-gray-50 dark:bg-gray-900">
|
||||
<div class="pb-8 w-full">
|
||||
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
@@ -690,6 +1005,165 @@ templ FileMetadataDetails(ctx context.Context, data FileMetadataDetailsData) {
|
||||
// FileMetadataSearch renders an advanced search form for file metadata
|
||||
templ FileMetadataSearch(ctx context.Context, data FileMetadataSearchData) {
|
||||
@LayoutWithContext("Search Files", ctx) {
|
||||
<!-- Status and Error Messages -->
|
||||
<div id="toast-container" class="fixed top-5 right-5 z-50 flex flex-col gap-2"></div>
|
||||
|
||||
<script>
|
||||
// Debug notification system
|
||||
console.log("File Search template loaded, setting up notification system");
|
||||
|
||||
// Function to create and show a toast
|
||||
function showToast(message, type) {
|
||||
const toastContainer = document.getElementById('toast-container');
|
||||
|
||||
// Create toast element
|
||||
const toast = document.createElement('div');
|
||||
toast.id = 'toast-' + type + '-' + Date.now();
|
||||
toast.className = 'flex items-center w-full max-w-xs p-4 mb-4 rounded-lg shadow text-gray-500 bg-white dark:text-gray-400 dark:bg-gray-800 transform translate-y-16 opacity-0 transition-all duration-300 ease-out';
|
||||
toast.role = 'alert';
|
||||
|
||||
// Set toast content based on type
|
||||
let iconClass, bgColorClass, textColorClass;
|
||||
|
||||
if (type === 'success') {
|
||||
iconClass = 'text-green-500 bg-green-100 dark:bg-green-800 dark:text-green-200';
|
||||
bgColorClass = 'text-green-500 dark:text-green-200';
|
||||
textColorClass = 'text-green-500 dark:text-green-200';
|
||||
} else if (type === 'error') {
|
||||
iconClass = 'text-red-500 bg-red-100 dark:bg-red-800 dark:text-red-200';
|
||||
bgColorClass = 'text-red-500 dark:text-red-200';
|
||||
textColorClass = 'text-red-500 dark:text-red-200';
|
||||
} else {
|
||||
iconClass = 'text-blue-500 bg-blue-100 dark:bg-blue-800 dark:text-blue-200';
|
||||
bgColorClass = 'text-blue-500 dark:text-blue-200';
|
||||
textColorClass = 'text-blue-500 dark:text-blue-200';
|
||||
}
|
||||
|
||||
// Set inner HTML with appropriate icon and message
|
||||
toast.innerHTML = `
|
||||
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 rounded-lg ${iconClass}">
|
||||
${type === 'success'
|
||||
? '<i class="fas fa-check"></i>'
|
||||
: type === 'error'
|
||||
? '<i class="fas fa-exclamation-circle"></i>'
|
||||
: '<i class="fas fa-info-circle"></i>'}
|
||||
</div>
|
||||
<div class="ml-3 text-sm font-normal">${message}</div>
|
||||
<button type="button" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" data-dismiss-target="#${toast.id}" aria-label="Close">
|
||||
<span class="sr-only">Close</span>
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
// Add toast to container
|
||||
toastContainer.appendChild(toast);
|
||||
|
||||
// Trigger animation after a small delay to ensure the DOM has updated
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('translate-y-16', 'opacity-0');
|
||||
toast.classList.add('translate-y-0', 'opacity-100');
|
||||
}, 10);
|
||||
|
||||
// Add event listener to close button
|
||||
const closeButton = toast.querySelector('button[data-dismiss-target]');
|
||||
closeButton.addEventListener('click', function() {
|
||||
// Animate out before removing
|
||||
toast.classList.add('opacity-0', 'translate-y-4');
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Auto-remove toast after 5 seconds
|
||||
setTimeout(() => {
|
||||
toast.classList.add('opacity-0', 'translate-y-4');
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Handle modal hide buttons
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const hideButtons = document.querySelectorAll('[data-modal-hide]');
|
||||
hideButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const modalId = this.getAttribute('data-modal-hide');
|
||||
const modal = document.getElementById(modalId);
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Track deletion events
|
||||
document.addEventListener('htmx:beforeRequest', function(event) {
|
||||
if (event.detail.path && event.detail.path.match(/^\/files\/\d+$/) && event.detail.verb === 'DELETE') {
|
||||
window.isFileDeleteRequest = true;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('htmx:afterRequest', function(event) {
|
||||
const isDeleteRequest =
|
||||
window.currentlyDeletingFile ||
|
||||
window.isFileDeleteRequest ||
|
||||
(event.detail.pathInfo && event.detail.pathInfo.requestPath &&
|
||||
event.detail.pathInfo.requestPath.match(/^\/files\/\d+$/) &&
|
||||
event.detail.verb === 'DELETE');
|
||||
|
||||
if (isDeleteRequest && event.detail.successful) {
|
||||
let fileName = "Unknown";
|
||||
|
||||
if (event.detail.elt && event.detail.elt.getAttribute) {
|
||||
fileName = event.detail.elt.getAttribute('data-file-name') || fileName;
|
||||
}
|
||||
|
||||
if (fileName === "Unknown" && window.lastDeletedFile) {
|
||||
fileName = window.lastDeletedFile.name;
|
||||
}
|
||||
|
||||
showToast(`File "${fileName}" deleted successfully`, 'success');
|
||||
|
||||
window.currentlyDeletingFile = false;
|
||||
window.isFileDeleteRequest = false;
|
||||
window.lastDeletedFile = null;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('htmx:responseError', function(event) {
|
||||
const isDeleteRequest =
|
||||
window.currentlyDeletingFile ||
|
||||
window.isFileDeleteRequest ||
|
||||
(event.detail.pathInfo && event.detail.pathInfo.requestPath &&
|
||||
event.detail.pathInfo.requestPath.match(/^\/files\/\d+$/) &&
|
||||
event.detail.verb === 'DELETE');
|
||||
|
||||
if (isDeleteRequest) {
|
||||
let fileName = "Unknown";
|
||||
|
||||
if (event.detail.elt && event.detail.elt.getAttribute) {
|
||||
fileName = event.detail.elt.getAttribute('data-file-name') || fileName;
|
||||
}
|
||||
|
||||
if (fileName === "Unknown" && window.lastDeletedFile) {
|
||||
fileName = window.lastDeletedFile.name;
|
||||
}
|
||||
|
||||
let errorMsg = `Failed to delete file "${fileName}"`;
|
||||
|
||||
if (event.detail.xhr && event.detail.xhr.responseText) {
|
||||
errorMsg = `Error: ${event.detail.xhr.responseText}`;
|
||||
}
|
||||
|
||||
showToast(errorMsg, 'error');
|
||||
|
||||
window.currentlyDeletingFile = false;
|
||||
window.isFileDeleteRequest = false;
|
||||
window.lastDeletedFile = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div id="file-search-container" style="min-height: 100vh;" class="bg-gray-50 dark:bg-gray-900">
|
||||
<div class="pb-8 w-full">
|
||||
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
|
||||
+116
-63
@@ -8,40 +8,31 @@ import (
|
||||
|
||||
// Dialog component for confirmation dialogs using Flowbite modal
|
||||
templ JobDialog(id string, title string, message string, confirmClass string, confirmText string, action string, jobID uint, jobName string) {
|
||||
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm z-50 flex items-center justify-center overflow-y-auto overflow-x-hidden">
|
||||
<div class="relative p-4 w-full max-w-md h-auto">
|
||||
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm">
|
||||
<div class="relative p-4 w-full max-w-md max-h-full mx-auto">
|
||||
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
|
||||
<h3 class="text-xl font-medium text-gray-900 dark:text-white flex items-center">
|
||||
<i class="fas fa-exclamation-triangle text-yellow-400 me-2 fa-2x"></i>
|
||||
{ title }
|
||||
</h3>
|
||||
<button type="button" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white" onclick={ hideJobDialog(id) }>
|
||||
<i class="fas fa-times"></i>
|
||||
<span class="sr-only">Close modal</span>
|
||||
<div class="p-6 text-center">
|
||||
if action == "delete" {
|
||||
<i class="fas fa-trash-alt text-red-400 text-3xl mb-4"></i>
|
||||
} else {
|
||||
<i class="fas fa-exclamation-triangle text-yellow-400 text-3xl mb-4"></i>
|
||||
}
|
||||
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">{ message }</h3>
|
||||
<button
|
||||
type="button"
|
||||
class={ confirmClass }
|
||||
hx-delete={ fmt.Sprintf("/jobs/%d", jobID) }
|
||||
hx-target="closest li"
|
||||
hx-swap="delete"
|
||||
data-job-name={ jobName }
|
||||
data-job-id={ fmt.Sprint(jobID) }
|
||||
id={ fmt.Sprintf("delete-btn-%d", jobID) }
|
||||
onclick={ triggerJobDelete(id, jobID, jobName) }>
|
||||
{ confirmText }
|
||||
</button>
|
||||
<button type="button" data-modal-hide={ id } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-4 md:p-5 text-center">
|
||||
<p class="text-gray-500 dark:text-gray-300 mb-5">
|
||||
{ message }
|
||||
</p>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" class="py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700" onclick={ hideJobDialog(id) }>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={ confirmClass }
|
||||
hx-delete={ fmt.Sprintf("/jobs/%d", jobID) }
|
||||
hx-target="closest li"
|
||||
hx-swap="delete"
|
||||
data-job-name={ jobName }
|
||||
data-job-id={ fmt.Sprint(jobID) }
|
||||
id={ fmt.Sprintf("delete-btn-%d", jobID) }
|
||||
onclick={ triggerJobDelete(id, jobID, jobName) }>
|
||||
{ confirmText }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -50,15 +41,18 @@ templ JobDialog(id string, title string, message string, confirmClass string, co
|
||||
|
||||
script hideJobDialog(id string) {
|
||||
document.getElementById(id).classList.add("hidden");
|
||||
document.getElementById(id).classList.remove("flex");
|
||||
}
|
||||
|
||||
script showJobDialog(id string) {
|
||||
document.getElementById(id).classList.remove("hidden");
|
||||
document.getElementById(id).classList.add("flex");
|
||||
}
|
||||
|
||||
script triggerJobDelete(dialogId string, jobID uint, jobName string) {
|
||||
// Hide the dialog
|
||||
document.getElementById(dialogId).classList.add("hidden");
|
||||
document.getElementById(dialogId).classList.remove("flex");
|
||||
|
||||
// Add debugging info
|
||||
console.log(`Job deletion triggered for: ${jobName} (ID: ${jobID})`);
|
||||
@@ -80,37 +74,83 @@ type JobsData struct {
|
||||
|
||||
templ Jobs(ctx context.Context, data JobsData) {
|
||||
@LayoutWithContext("Scheduled Jobs", ctx) {
|
||||
<!-- Status and Error Messages -->
|
||||
<div id="toast-container" class="fixed top-5 right-5 z-50 flex flex-col gap-2"></div>
|
||||
|
||||
<script>
|
||||
// Debug notification system
|
||||
console.log("Jobs template loaded, setting up notification system");
|
||||
|
||||
// Create a global notyf instance immediately
|
||||
window.notyf = new Notyf({
|
||||
duration: 3000,
|
||||
position: {
|
||||
x: 'right',
|
||||
y: 'top',
|
||||
},
|
||||
types: [
|
||||
{
|
||||
type: 'success',
|
||||
background: '#38c172',
|
||||
icon: {
|
||||
className: 'fas fa-check-circle',
|
||||
tagName: 'i'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'error',
|
||||
background: '#e3342f',
|
||||
icon: {
|
||||
className: 'fas fa-exclamation-circle',
|
||||
tagName: 'i'
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
console.log("Notyf initialized:", window.notyf);
|
||||
// Function to create and show a toast
|
||||
function showToast(message, type) {
|
||||
const toastContainer = document.getElementById('toast-container');
|
||||
|
||||
// Create toast element
|
||||
const toast = document.createElement('div');
|
||||
toast.id = 'toast-' + type + '-' + Date.now();
|
||||
toast.className = 'flex items-center w-full max-w-xs p-4 mb-4 rounded-lg shadow text-gray-500 bg-white dark:text-gray-400 dark:bg-gray-800 transform translate-y-16 opacity-0 transition-all duration-300 ease-out';
|
||||
toast.role = 'alert';
|
||||
|
||||
// Set toast content based on type
|
||||
let iconClass, bgColorClass, textColorClass;
|
||||
|
||||
if (type === 'success') {
|
||||
iconClass = 'text-green-500 bg-green-100 dark:bg-green-800 dark:text-green-200';
|
||||
bgColorClass = 'text-green-500 dark:text-green-200';
|
||||
textColorClass = 'text-green-500 dark:text-green-200';
|
||||
} else if (type === 'error') {
|
||||
iconClass = 'text-red-500 bg-red-100 dark:bg-red-800 dark:text-red-200';
|
||||
bgColorClass = 'text-red-500 dark:text-red-200';
|
||||
textColorClass = 'text-red-500 dark:text-red-200';
|
||||
} else {
|
||||
iconClass = 'text-blue-500 bg-blue-100 dark:bg-blue-800 dark:text-blue-200';
|
||||
bgColorClass = 'text-blue-500 dark:text-blue-200';
|
||||
textColorClass = 'text-blue-500 dark:text-blue-200';
|
||||
}
|
||||
|
||||
// Set inner HTML with appropriate icon and message
|
||||
toast.innerHTML = `
|
||||
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 rounded-lg ${iconClass}">
|
||||
${type === 'success'
|
||||
? '<i class="fas fa-check"></i>'
|
||||
: type === 'error'
|
||||
? '<i class="fas fa-exclamation-circle"></i>'
|
||||
: '<i class="fas fa-info-circle"></i>'}
|
||||
</div>
|
||||
<div class="ml-3 text-sm font-normal">${message}</div>
|
||||
<button type="button" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" data-dismiss-target="#${toast.id}" aria-label="Close">
|
||||
<span class="sr-only">Close</span>
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
// Add toast to container
|
||||
toastContainer.appendChild(toast);
|
||||
|
||||
// Trigger animation after a small delay to ensure the DOM has updated
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('translate-y-16', 'opacity-0');
|
||||
toast.classList.add('translate-y-0', 'opacity-100');
|
||||
}, 10);
|
||||
|
||||
// Add event listener to close button
|
||||
const closeButton = toast.querySelector('button[data-dismiss-target]');
|
||||
closeButton.addEventListener('click', function() {
|
||||
// Animate out before removing
|
||||
toast.classList.add('opacity-0', 'translate-y-4');
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Auto-remove toast after 5 seconds
|
||||
setTimeout(() => {
|
||||
toast.classList.add('opacity-0', 'translate-y-4');
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Global function to handle job running
|
||||
window.runJob = function(button) {
|
||||
@@ -130,18 +170,31 @@ templ Jobs(ctx context.Context, data JobsData) {
|
||||
displayName = event.detail.headers['HX-Job-Name'];
|
||||
}
|
||||
console.log(`Showing success notification for job: ${displayName}`);
|
||||
window.notyf.success(`Job "${displayName}" started successfully!`);
|
||||
showToast(`Job "${displayName}" started successfully!`, 'success');
|
||||
} else {
|
||||
let errorMsg = `Failed to start job "${jobName}"`;
|
||||
if (event.detail.xhr && event.detail.xhr.responseText) {
|
||||
errorMsg = `Error: ${event.detail.xhr.responseText}`;
|
||||
}
|
||||
console.log(`Showing error notification: ${errorMsg}`);
|
||||
window.notyf.error(errorMsg);
|
||||
showToast(errorMsg, 'error');
|
||||
}
|
||||
}, { once: true });
|
||||
};
|
||||
|
||||
// Handle modal hide buttons
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const hideButtons = document.querySelectorAll('[data-modal-hide]');
|
||||
hideButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const modalId = this.getAttribute('data-modal-hide');
|
||||
const modal = document.getElementById(modalId);
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Track all HTMX events for debugging
|
||||
document.addEventListener('htmx:beforeRequest', function(event) {
|
||||
console.log("HTMX before request:", event.detail);
|
||||
@@ -195,7 +248,7 @@ templ Jobs(ctx context.Context, data JobsData) {
|
||||
}
|
||||
|
||||
console.log(`Showing success notification for deleted job: ${jobName}`);
|
||||
window.notyf.success(`Job "${jobName}" deleted successfully`);
|
||||
showToast(`Job "${jobName}" deleted successfully`, 'success');
|
||||
|
||||
// Clear flags
|
||||
window.currentlyDeletingJob = false;
|
||||
@@ -238,7 +291,7 @@ templ Jobs(ctx context.Context, data JobsData) {
|
||||
}
|
||||
|
||||
console.log(`Showing error notification: ${errorMsg}`);
|
||||
window.notyf.error(errorMsg);
|
||||
showToast(errorMsg, 'error');
|
||||
|
||||
// Clear flags
|
||||
window.currentlyDeletingJob = false;
|
||||
|
||||
@@ -2,6 +2,7 @@ package components
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/starfleetcptn/gomft/internal/db"
|
||||
"strconv"
|
||||
)
|
||||
@@ -27,6 +28,225 @@ templ UserManagementContent(data UserManagementData) {
|
||||
// <div id="userManagementContainer" class="px-4 py-8 mx-auto max-w-screen-xl lg:px-6">
|
||||
<div id="userManagementContainer" style="min-height: 100vh;" class="bg-gray-50 dark:bg-gray-900">
|
||||
<div class="pb-8 w-full">
|
||||
<!-- Toast container for status messages -->
|
||||
<div id="toast-container" class="fixed top-5 right-5 z-50 flex flex-col gap-2"></div>
|
||||
|
||||
<script>
|
||||
// Function to create and show a toast
|
||||
function showToast(message, type) {
|
||||
const toastContainer = document.getElementById('toast-container');
|
||||
|
||||
// Create toast element
|
||||
const toast = document.createElement('div');
|
||||
toast.id = 'toast-' + type + '-' + Date.now();
|
||||
toast.className = 'flex items-center w-full max-w-xs p-4 mb-4 rounded-lg shadow text-gray-500 bg-white dark:text-gray-400 dark:bg-gray-800 transform translate-y-16 opacity-0 transition-all duration-300 ease-out';
|
||||
toast.role = 'alert';
|
||||
|
||||
// Set toast content based on type
|
||||
let iconClass, bgColorClass, textColorClass;
|
||||
|
||||
if (type === 'success') {
|
||||
iconClass = 'text-green-500 bg-green-100 dark:bg-green-800 dark:text-green-200';
|
||||
bgColorClass = 'text-green-500 dark:text-green-200';
|
||||
textColorClass = 'text-green-500 dark:text-green-200';
|
||||
} else if (type === 'error') {
|
||||
iconClass = 'text-red-500 bg-red-100 dark:bg-red-800 dark:text-red-200';
|
||||
bgColorClass = 'text-red-500 dark:text-red-200';
|
||||
textColorClass = 'text-red-500 dark:text-red-200';
|
||||
} else {
|
||||
iconClass = 'text-blue-500 bg-blue-100 dark:bg-blue-800 dark:text-blue-200';
|
||||
bgColorClass = 'text-blue-500 dark:text-blue-200';
|
||||
textColorClass = 'text-blue-500 dark:text-blue-200';
|
||||
}
|
||||
|
||||
// Set inner HTML with appropriate icon and message
|
||||
toast.innerHTML = `
|
||||
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 rounded-lg ${iconClass}">
|
||||
${type === 'success'
|
||||
? '<i class="fas fa-check"></i>'
|
||||
: type === 'error'
|
||||
? '<i class="fas fa-exclamation-circle"></i>'
|
||||
: '<i class="fas fa-info-circle"></i>'}
|
||||
</div>
|
||||
<div class="ml-3 text-sm font-normal">${message}</div>
|
||||
<button type="button" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" data-dismiss-target="#${toast.id}" aria-label="Close">
|
||||
<span class="sr-only">Close</span>
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
// Add toast to container
|
||||
toastContainer.appendChild(toast);
|
||||
|
||||
// Trigger animation after a small delay to ensure the DOM has updated
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('translate-y-16', 'opacity-0');
|
||||
toast.classList.add('translate-y-0', 'opacity-100');
|
||||
}, 10);
|
||||
|
||||
// Add event listener to close button
|
||||
const closeButton = toast.querySelector('button[data-dismiss-target]');
|
||||
closeButton.addEventListener('click', function() {
|
||||
// Animate out before removing
|
||||
toast.classList.add('opacity-0', 'translate-y-4');
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Auto-remove toast after 5 seconds
|
||||
setTimeout(() => {
|
||||
toast.classList.add('opacity-0', 'translate-y-4');
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Handle modal hide buttons
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const hideButtons = document.querySelectorAll('[data-modal-hide]');
|
||||
hideButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const modalId = this.getAttribute('data-modal-hide');
|
||||
const modal = document.getElementById(modalId);
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Track user operations in HTMX events
|
||||
document.addEventListener('htmx:beforeRequest', function(event) {
|
||||
console.log("HTMX before request:", event.detail);
|
||||
|
||||
// Check if this is a DELETE request for a user
|
||||
const path = event.detail.path;
|
||||
const method = event.detail.verb;
|
||||
|
||||
console.log(`Request path: ${path}, method: ${method}`);
|
||||
|
||||
// Pattern match for user deletions (e.g., /admin/users/123)
|
||||
if (path && method === 'DELETE' && path.match(/^\/admin\/users\/\d+$/)) {
|
||||
console.log("Detected user deletion request via URL pattern");
|
||||
|
||||
// This is definitely a delete request - store this information
|
||||
window.isUserDeleteRequest = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Track HTMX events for user operations
|
||||
document.addEventListener('htmx:afterRequest', function(event) {
|
||||
// Check if the request was successful
|
||||
if (event.detail.successful) {
|
||||
// Check for POST, PUT, DELETE operations
|
||||
const path = event.detail.pathInfo?.requestPath || '';
|
||||
const method = event.detail.verb || '';
|
||||
|
||||
// User deletion handling
|
||||
const isDeleteRequest =
|
||||
window.currentlyDeletingUser ||
|
||||
window.isUserDeleteRequest ||
|
||||
(method === 'DELETE' && path.match(/^\/admin\/users\/\d+$/));
|
||||
|
||||
if (isDeleteRequest) {
|
||||
let userEmail = "user";
|
||||
|
||||
// Try multiple sources for user email
|
||||
if (event.detail.elt && event.detail.elt.getAttribute) {
|
||||
userEmail = event.detail.elt.getAttribute('data-user-email') || userEmail;
|
||||
}
|
||||
|
||||
if (userEmail === "user" && window.lastDeletedUser) {
|
||||
// Fallback to our stored user info
|
||||
userEmail = window.lastDeletedUser.email;
|
||||
}
|
||||
|
||||
showToast(`User "${userEmail}" deleted successfully`, 'success');
|
||||
|
||||
// Clear flags
|
||||
window.currentlyDeletingUser = false;
|
||||
window.isUserDeleteRequest = false;
|
||||
window.lastDeletedUser = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// User creation
|
||||
if (method === 'POST' && path === '/admin/users') {
|
||||
// Find the email from the form data if possible
|
||||
const formData = event.detail.requestConfig?.parameters;
|
||||
let email = "New user";
|
||||
if (formData && formData.email) {
|
||||
email = formData.email;
|
||||
}
|
||||
showToast(`User "${email}" created successfully`, 'success');
|
||||
return;
|
||||
}
|
||||
|
||||
// User update
|
||||
if (method === 'PUT' && path.match(/^\/admin\/users\/\d+$/)) {
|
||||
// Find the email from the form data if possible
|
||||
const formData = event.detail.requestConfig?.parameters;
|
||||
let email = "User";
|
||||
if (formData && formData.email) {
|
||||
email = formData.email;
|
||||
}
|
||||
showToast(`User "${email}" updated successfully`, 'success');
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Track HTMX error events
|
||||
document.addEventListener('htmx:responseError', function(event) {
|
||||
console.log("HTMX response error:", event.detail);
|
||||
|
||||
// Check for delete request errors
|
||||
const isDeleteRequest =
|
||||
window.currentlyDeletingUser ||
|
||||
window.isUserDeleteRequest ||
|
||||
(event.detail.requestConfig &&
|
||||
event.detail.requestConfig.verb === 'DELETE' &&
|
||||
event.detail.pathInfo?.requestPath?.match(/^\/admin\/users\/\d+$/));
|
||||
|
||||
if (isDeleteRequest) {
|
||||
let userEmail = "user";
|
||||
|
||||
// Try multiple sources for user email
|
||||
if (event.detail.elt && event.detail.elt.getAttribute) {
|
||||
userEmail = event.detail.elt.getAttribute('data-user-email') || userEmail;
|
||||
}
|
||||
|
||||
if (userEmail === "user" && window.lastDeletedUser) {
|
||||
userEmail = window.lastDeletedUser.email;
|
||||
}
|
||||
|
||||
let errorMsg = `Failed to delete user "${userEmail}"`;
|
||||
|
||||
if (event.detail.xhr && event.detail.xhr.responseText) {
|
||||
errorMsg = `Error: ${event.detail.xhr.responseText}`;
|
||||
}
|
||||
|
||||
showToast(errorMsg, 'error');
|
||||
|
||||
// Clear flags
|
||||
window.currentlyDeletingUser = false;
|
||||
window.isUserDeleteRequest = false;
|
||||
window.lastDeletedUser = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Generic error handling
|
||||
let errorMsg = 'An error occurred';
|
||||
|
||||
if (event.detail.xhr && event.detail.xhr.responseText) {
|
||||
errorMsg = `Error: ${event.detail.xhr.responseText}`;
|
||||
}
|
||||
|
||||
showToast(errorMsg, 'error');
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||
@@ -45,19 +265,45 @@ templ UserManagementContent(data UserManagementData) {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Status and Error Messages -->
|
||||
|
||||
<!-- Check for URL parameters after page load to show success/error messages -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Get URL parameters
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const successMsg = urlParams.get('success');
|
||||
const errorMsg = urlParams.get('error');
|
||||
|
||||
// Show success toast if parameter exists
|
||||
if (successMsg) {
|
||||
showToast(decodeURIComponent(successMsg), 'success');
|
||||
}
|
||||
|
||||
// Show error toast if parameter exists
|
||||
if (errorMsg) {
|
||||
showToast(decodeURIComponent(errorMsg), 'error');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Convert existing messages to toasts if present -->
|
||||
if data.SuccessMessage != "" {
|
||||
<div id="status-message" class="mb-4 p-4 text-sm text-green-700 bg-green-100 rounded-lg dark:bg-green-200 dark:text-green-800" role="alert">
|
||||
<div class="font-medium">{ data.SuccessMessage }</div>
|
||||
</div>
|
||||
} else {
|
||||
<div id="status-message" class="hidden mb-4 p-4 text-sm text-green-700 bg-green-100 rounded-lg dark:bg-green-200 dark:text-green-800" role="alert"></div>
|
||||
<script>
|
||||
// Show success message as toast
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
showToast("{ data.SuccessMessage }", 'success');
|
||||
});
|
||||
</script>
|
||||
}
|
||||
if data.ErrorMessage != "" {
|
||||
<div id="error-message" class="mb-4 p-4 text-sm text-red-700 bg-red-100 rounded-lg dark:bg-red-200 dark:text-red-800" role="alert">
|
||||
<div class="font-medium">{ data.ErrorMessage }</div>
|
||||
</div>
|
||||
<script>
|
||||
// Show error message as toast
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
showToast("{ data.ErrorMessage }", 'error');
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
||||
<!-- Main Content Container -->
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 dark:bg-gray-800">
|
||||
<!-- Include the appropriate tab -->
|
||||
@@ -85,6 +331,20 @@ templ UserManagementContent(data UserManagementData) {
|
||||
|
||||
// User listing component
|
||||
templ userList(users []db.User) {
|
||||
<!-- Dialog Container for User Actions -->
|
||||
for _, user := range users {
|
||||
@UserDialog(
|
||||
fmt.Sprintf("delete-user-dialog-%d", user.ID),
|
||||
"Delete User",
|
||||
fmt.Sprintf("Are you sure you want to delete user \"%s\"? This action cannot be undone.", user.Email),
|
||||
"bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700",
|
||||
"Delete User",
|
||||
"delete",
|
||||
user.ID,
|
||||
user.Email,
|
||||
)
|
||||
}
|
||||
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
|
||||
@@ -159,9 +419,9 @@ templ userList(users []db.User) {
|
||||
</button>
|
||||
<button
|
||||
class="text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-3 py-1.5 text-center inline-flex items-center dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800"
|
||||
hx-delete={ "/admin/users/" + strconv.Itoa(int(user.ID)) }
|
||||
hx-confirm="Are you sure you want to delete this user? This action cannot be undone."
|
||||
hx-target="#userManagementContainer"
|
||||
data-user-email={ user.Email }
|
||||
data-user-id={ strconv.Itoa(int(user.ID)) }
|
||||
onclick={ showUserDialog(fmt.Sprintf("delete-user-dialog-%d", user.ID)) }
|
||||
>
|
||||
<i class="fas fa-trash-alt w-3.5 h-3.5 mr-1.5"></i> Delete
|
||||
</button>
|
||||
@@ -371,3 +631,59 @@ func hasRole(user *db.User, roleID uint) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// UserDialog for confirmation actions
|
||||
templ UserDialog(id string, title string, message string, confirmClass string, confirmText string, action string, userID uint, userEmail string) {
|
||||
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm">
|
||||
<div class="relative p-4 w-full max-w-md max-h-full mx-auto">
|
||||
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||
<div class="p-6 text-center">
|
||||
if action == "delete" {
|
||||
<i class="fas fa-user-slash text-red-400 text-3xl mb-4"></i>
|
||||
} else {
|
||||
<i class="fas fa-exclamation-triangle text-yellow-400 text-3xl mb-4"></i>
|
||||
}
|
||||
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">{ message }</h3>
|
||||
<button
|
||||
type="button"
|
||||
class="text-white font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 bg-red-600 hover:bg-red-700 focus:ring-4 focus:outline-none focus:ring-red-300 dark:bg-red-500 dark:hover:bg-red-600 dark:focus:ring-red-800"
|
||||
hx-delete={ fmt.Sprintf("/admin/users/%d", userID) }
|
||||
hx-target="#userManagementContainer"
|
||||
data-user-email={ userEmail }
|
||||
id={ fmt.Sprintf("delete-user-btn-%d", userID) }
|
||||
onclick={ triggerUserDelete(id, userID, userEmail) }>
|
||||
{ confirmText }
|
||||
</button>
|
||||
<button type="button" data-modal-hide={ id } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
script hideUserDialog(id string) {
|
||||
document.getElementById(id).classList.add("hidden");
|
||||
document.getElementById(id).classList.remove("flex");
|
||||
}
|
||||
|
||||
script showUserDialog(id string) {
|
||||
document.getElementById(id).classList.remove("hidden");
|
||||
document.getElementById(id).classList.add("flex");
|
||||
}
|
||||
|
||||
script triggerUserDelete(dialogId string, userID uint, userEmail string) {
|
||||
// Hide the dialog
|
||||
document.getElementById(dialogId).classList.add("hidden");
|
||||
document.getElementById(dialogId).classList.remove("flex");
|
||||
|
||||
// Store data in a way that's accessible to event handlers
|
||||
window.lastDeletedUser = {
|
||||
id: userID,
|
||||
email: userEmail
|
||||
};
|
||||
|
||||
// Add custom marker to track this deletion
|
||||
window.currentlyDeletingUser = true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user