mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-10-24 06:50:26 +02:00
feat: Enhance file metadata and notification components
- Added sorting capabilities to file metadata lists, including SortBy and SortDir fields. - Integrated a toast notification system for user feedback on actions. - Updated templates and JavaScript for improved user interaction and responsiveness. - Refactored file metadata handlers to utilize new sorting features and ensure proper data flow. - Introduced new notification dialog components for better user confirmation on actions.
This commit is contained in:
1 parent
c52edb75df
commit
28fa65df9d
49 files changed
+6153
-926
No files matched your search
@@ -1,91 +1,114 @@
|
||||
package utils
|
||||
|
||||
// fileMetadataJS provides common JavaScript functions for file metadata components
|
||||
templ fileMetadataJS() {
|
||||
// FileMetadataJS provides common JavaScript functions for file metadata components,
|
||||
// including HTMX event listeners for delete toasts.
|
||||
templ FileMetadataJS() {
|
||||
<script>
|
||||
// Show a modal by ID
|
||||
function showModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
}
|
||||
}
|
||||
|
||||
// Close a modal by ID
|
||||
function closeModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
}
|
||||
}
|
||||
|
||||
// Show a toast notification
|
||||
// Function to create and show a toast (Defined locally for guaranteed availability)
|
||||
function showToast(message, type = 'info') {
|
||||
const container = document.getElementById('toast-container');
|
||||
if (!container) return;
|
||||
const toastContainer = document.getElementById('toast-container');
|
||||
if (!toastContainer) {
|
||||
console.error("Toast container not found!"); // Keep this error log
|
||||
return;
|
||||
}
|
||||
|
||||
// Create toast element
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `p-4 rounded-lg shadow-lg ${
|
||||
type === 'success' ? 'bg-green-500' :
|
||||
type === 'error' ? 'bg-red-500' :
|
||||
type === 'warning' ? 'bg-yellow-500' :
|
||||
'bg-blue-500'
|
||||
} text-white`;
|
||||
toast.textContent = message;
|
||||
toast.id = 'toast-' + type + '-' + Date.now();
|
||||
// Use classes similar to the original file_metadata.templ for consistency
|
||||
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';
|
||||
|
||||
container.appendChild(toast);
|
||||
// Set toast content based on type
|
||||
let iconClass;
|
||||
if (type === 'success') {
|
||||
iconClass = 'text-green-500 bg-green-100 dark:bg-green-800 dark:text-green-200';
|
||||
} else if (type === 'error') {
|
||||
iconClass = 'text-red-500 bg-red-100 dark:bg-red-800 dark:text-red-200';
|
||||
} else { // Default to info
|
||||
iconClass = 'text-blue-500 bg-blue-100 dark:bg-blue-800 dark:text-blue-200';
|
||||
}
|
||||
|
||||
// Remove the toast after 5 seconds
|
||||
// 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
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
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() {
|
||||
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);
|
||||
}
|
||||
|
||||
// Close modal when clicking outside
|
||||
document.addEventListener('click', function(event) {
|
||||
if (event.target.classList.contains('fixed')) {
|
||||
event.target.classList.add('hidden');
|
||||
event.target.classList.remove('flex');
|
||||
}
|
||||
});
|
||||
// --- HTMX Event Listener for Delete Toasts ---
|
||||
|
||||
// Handle delete confirmation
|
||||
document.addEventListener('click', function(event) {
|
||||
if (event.target.id === 'confirm-delete') {
|
||||
const fileId = event.target.dataset.fileId;
|
||||
if (!fileId) return;
|
||||
// Ensure listener is attached only once using a flag
|
||||
if (!window._gomft_fileMetadataListenerAttached) {
|
||||
document.body.addEventListener('htmx:afterRequest', function(event) {
|
||||
const triggerElement = event.detail.elt;
|
||||
|
||||
fetch(`/files/${fileId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
showToast('File deleted successfully', 'success');
|
||||
// Reload the page after a short delay
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
throw new Error('Failed to delete file');
|
||||
// Check if the element that triggered this request was the file delete button from the dialog
|
||||
if (triggerElement && triggerElement.id && triggerElement.id.startsWith('delete-file-btn-')) {
|
||||
// Get the path directly from the element's hx-delete attribute
|
||||
const path = triggerElement.getAttribute('hx-delete');
|
||||
// Ensure requestConfig exists before accessing verb (robustness)
|
||||
const method = event.detail.requestConfig ? event.detail.requestConfig.verb : null;
|
||||
|
||||
// Check if the method was delete and the path from the attribute matches the expected pattern
|
||||
if (method === 'delete' && path && path.match(/^\/files\/\d+$/)) {
|
||||
const fileName = triggerElement.getAttribute('data-file-name') || "Unknown"; // Get filename from the button
|
||||
|
||||
// Call the locally defined showToast function
|
||||
if (event.detail.successful) {
|
||||
showToast(`File "${fileName}" metadata deleted successfully`, 'success');
|
||||
} else {
|
||||
let errorMsg = `Failed to delete file "${fileName}" metadata`;
|
||||
if (event.detail.xhr && event.detail.xhr.responseText) {
|
||||
try {
|
||||
const responseJson = JSON.parse(event.detail.xhr.responseText);
|
||||
errorMsg = responseJson.error ? `Error: ${responseJson.error}` : `Error: ${event.detail.xhr.responseText}`;
|
||||
} catch (e) {
|
||||
errorMsg = `Error: ${event.detail.xhr.responseText}`;
|
||||
}
|
||||
}
|
||||
showToast(errorMsg, 'error');
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showToast(error.message, 'error');
|
||||
})
|
||||
.finally(() => {
|
||||
closeModal('delete-dialog');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Set the flag to true after attaching the listener
|
||||
window._gomft_fileMetadataListenerAttached = true;
|
||||
console.log("[FileMetadataJS] HTMX afterRequest listener attached."); // Log attachment once
|
||||
}
|
||||
|
||||
// Handle HTMX request errors
|
||||
document.body.addEventListener('htmx:responseError', function(evt) {
|
||||
showToast('An error occurred while processing your request', 'error');
|
||||
});
|
||||
</script>
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,9 @@ package utils
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
// fileMetadataJS provides common JavaScript functions for file metadata components
|
||||
func fileMetadataJS() templ.Component {
|
||||
// FileMetadataJS provides common JavaScript functions for file metadata components,
|
||||
// including HTMX event listeners for delete toasts.
|
||||
func FileMetadataJS() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
@@ -30,7 +31,7 @@ func fileMetadataJS() templ.Component {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<script>\n\t\t// Show a modal by ID\n\t\tfunction showModal(modalId) {\n\t\t\tconst modal = document.getElementById(modalId);\n\t\t\tif (modal) {\n\t\t\t\tmodal.classList.remove('hidden');\n\t\t\t\tmodal.classList.add('flex');\n\t\t\t}\n\t\t}\n\n\t\t// Close a modal by ID\n\t\tfunction closeModal(modalId) {\n\t\t\tconst modal = document.getElementById(modalId);\n\t\t\tif (modal) {\n\t\t\t\tmodal.classList.add('hidden');\n\t\t\t\tmodal.classList.remove('flex');\n\t\t\t}\n\t\t}\n\n\t\t// Show a toast notification\n\t\tfunction showToast(message, type = 'info') {\n\t\t\tconst container = document.getElementById('toast-container');\n\t\t\tif (!container) return;\n\n\t\t\tconst toast = document.createElement('div');\n\t\t\ttoast.className = `p-4 rounded-lg shadow-lg ${\n\t\t\t\ttype === 'success' ? 'bg-green-500' :\n\t\t\t\ttype === 'error' ? 'bg-red-500' :\n\t\t\t\ttype === 'warning' ? 'bg-yellow-500' :\n\t\t\t\t'bg-blue-500'\n\t\t\t} text-white`;\n\t\t\ttoast.textContent = message;\n\n\t\t\tcontainer.appendChild(toast);\n\n\t\t\t// Remove the toast after 5 seconds\n\t\t\tsetTimeout(() => {\n\t\t\t\ttoast.remove();\n\t\t\t}, 5000);\n\t\t}\n\n\t\t// Close modal when clicking outside\n\t\tdocument.addEventListener('click', function(event) {\n\t\t\tif (event.target.classList.contains('fixed')) {\n\t\t\t\tevent.target.classList.add('hidden');\n\t\t\t\tevent.target.classList.remove('flex');\n\t\t\t}\n\t\t});\n\n\t\t// Handle delete confirmation\n\t\tdocument.addEventListener('click', function(event) {\n\t\t\tif (event.target.id === 'confirm-delete') {\n\t\t\t\tconst fileId = event.target.dataset.fileId;\n\t\t\t\tif (!fileId) return;\n\n\t\t\t\tfetch(`/files/${fileId}`, {\n\t\t\t\t\tmethod: 'DELETE',\n\t\t\t\t\theaders: {\n\t\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\t.then(response => {\n\t\t\t\t\tif (response.ok) {\n\t\t\t\t\t\tshowToast('File deleted successfully', 'success');\n\t\t\t\t\t\t// Reload the page after a short delay\n\t\t\t\t\t\tsetTimeout(() => {\n\t\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t\t}, 1000);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthrow new Error('Failed to delete file');\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.catch(error => {\n\t\t\t\t\tshowToast(error.message, 'error');\n\t\t\t\t})\n\t\t\t\t.finally(() => {\n\t\t\t\t\tcloseModal('delete-dialog');\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\n\t\t// Handle HTMX request errors\n\t\tdocument.body.addEventListener('htmx:responseError', function(evt) {\n\t\t\tshowToast('An error occurred while processing your request', 'error');\n\t\t});\n\t</script>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<script>\n\t\t// Function to create and show a toast (Defined locally for guaranteed availability)\n\t\tfunction showToast(message, type = 'info') {\n\t\t\tconst toastContainer = document.getElementById('toast-container');\n\t\t\tif (!toastContainer) {\n\t\t\t\tconsole.error(\"Toast container not found!\"); // Keep this error log\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Create toast element\n\t\t\tconst toast = document.createElement('div');\n\t\t\ttoast.id = 'toast-' + type + '-' + Date.now();\n\t\t\t// Use classes similar to the original file_metadata.templ for consistency\n\t\t\ttoast.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';\n\t\t\ttoast.role = 'alert';\n\n\t\t\t// Set toast content based on type\n\t\t\tlet iconClass;\n\t\t\tif (type === 'success') {\n\t\t\t\ticonClass = 'text-green-500 bg-green-100 dark:bg-green-800 dark:text-green-200';\n\t\t\t} else if (type === 'error') {\n\t\t\t\ticonClass = 'text-red-500 bg-red-100 dark:bg-red-800 dark:text-red-200';\n\t\t\t} else { // Default to info\n\t\t\t\ticonClass = 'text-blue-500 bg-blue-100 dark:bg-blue-800 dark:text-blue-200';\n\t\t\t}\n\n\t\t\t// Set inner HTML with appropriate icon and message\n\t\t\ttoast.innerHTML = `\n\t\t\t\t<div class=\"inline-flex items-center justify-center flex-shrink-0 w-8 h-8 rounded-lg ${iconClass}\">\n\t\t\t\t\t${type === 'success'\n\t\t\t\t\t\t? '<i class=\"fas fa-check\"></i>'\n\t\t\t\t\t\t: type === 'error'\n\t\t\t\t\t\t? '<i class=\"fas fa-exclamation-circle\"></i>'\n\t\t\t\t\t\t: '<i class=\"fas fa-info-circle\"></i>'}\n\t\t\t\t</div>\n\t\t\t\t<div class=\"ml-3 text-sm font-normal\">${message}</div>\n\t\t\t\t<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\">\n\t\t\t\t\t<span class=\"sr-only\">Close</span>\n\t\t\t\t\t<i class=\"fas fa-times\"></i>\n\t\t\t\t</button>\n\t\t\t`;\n\n\t\t\t// Add toast to container\n\t\t\ttoastContainer.appendChild(toast);\n\n\t\t\t// Trigger animation after a small delay\n\t\t\tsetTimeout(() => {\n\t\t\t\ttoast.classList.remove('translate-y-16', 'opacity-0');\n\t\t\t\ttoast.classList.add('translate-y-0', 'opacity-100');\n\t\t\t}, 10);\n\n\t\t\t// Add event listener to close button\n\t\t\tconst closeButton = toast.querySelector('button[data-dismiss-target]');\n\t\t\tcloseButton.addEventListener('click', function() {\n\t\t\t\ttoast.classList.add('opacity-0', 'translate-y-4');\n\t\t\t\tsetTimeout(() => { toast.remove(); }, 300);\n\t\t\t});\n\n\t\t\t// Auto-remove toast after 5 seconds\n\t\t\tsetTimeout(() => {\n\t\t\t\ttoast.classList.add('opacity-0', 'translate-y-4');\n\t\t\t\tsetTimeout(() => { toast.remove(); }, 300);\n\t\t\t}, 5000);\n\t\t}\n\n\t\t// --- HTMX Event Listener for Delete Toasts ---\n\n\t\t// Ensure listener is attached only once using a flag\n\t\tif (!window._gomft_fileMetadataListenerAttached) {\n\t\t\tdocument.body.addEventListener('htmx:afterRequest', function(event) {\n\t\t\t\tconst triggerElement = event.detail.elt;\n\n\t\t\t\t// Check if the element that triggered this request was the file delete button from the dialog\n\t\t\t\tif (triggerElement && triggerElement.id && triggerElement.id.startsWith('delete-file-btn-')) {\n\t\t\t\t\t // Get the path directly from the element's hx-delete attribute\n\t\t\t\t\t const path = triggerElement.getAttribute('hx-delete');\n\t\t\t\t\t // Ensure requestConfig exists before accessing verb (robustness)\n\t\t\t\t\t const method = event.detail.requestConfig ? event.detail.requestConfig.verb : null;\n\n\t\t\t\t\t // Check if the method was delete and the path from the attribute matches the expected pattern\n\t\t\t\t\t if (method === 'delete' && path && path.match(/^\\/files\\/\\d+$/)) {\n\t\t\t\t\t\tconst fileName = triggerElement.getAttribute('data-file-name') || \"Unknown\"; // Get filename from the button\n\n\t\t\t\t\t\t// Call the locally defined showToast function\n\t\t\t\t\t\tif (event.detail.successful) {\n\t\t\t\t\t\t\tshowToast(`File \"${fileName}\" metadata deleted successfully`, 'success');\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tlet errorMsg = `Failed to delete file \"${fileName}\" metadata`;\n\t\t\t\t\t\t\tif (event.detail.xhr && event.detail.xhr.responseText) {\n\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\tconst responseJson = JSON.parse(event.detail.xhr.responseText);\n\t\t\t\t\t\t\t\t\terrorMsg = responseJson.error ? `Error: ${responseJson.error}` : `Error: ${event.detail.xhr.responseText}`;\n\t\t\t\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\t\t\t\terrorMsg = `Error: ${event.detail.xhr.responseText}`;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\tLine truncated
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user