mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-10-12 17:27:28 +02:00
Add delete after transfer option and enhanced job/config deletion UX
This commit is contained in:
1 parent
f4c8c2121c
commit
81557840dd
11 files changed
+659
-23
No files matched your search
@@ -60,6 +60,7 @@ func getInitialData(config *db.TransferConfig) string {
|
||||
destPassiveMode := true
|
||||
archivePath := ""
|
||||
archiveEnabled := false
|
||||
deleteAfterTransfer := false
|
||||
rcloneFlags := ""
|
||||
|
||||
if config != nil {
|
||||
@@ -104,6 +105,7 @@ func getInitialData(config *db.TransferConfig) string {
|
||||
destPassiveMode = config.DestPassiveMode
|
||||
archivePath = config.ArchivePath
|
||||
archiveEnabled = config.ArchiveEnabled
|
||||
deleteAfterTransfer = config.DeleteAfterTransfer
|
||||
rcloneFlags = config.RcloneFlags
|
||||
}
|
||||
|
||||
@@ -147,6 +149,7 @@ func getInitialData(config *db.TransferConfig) string {
|
||||
// Existing fields
|
||||
archivePath: '%s',
|
||||
archiveEnabled: %v,
|
||||
deleteAfterTransfer: %v,
|
||||
rcloneFlags: '%s',
|
||||
loading: false,
|
||||
validate() {
|
||||
@@ -209,7 +212,7 @@ func getInitialData(config *db.TransferConfig) string {
|
||||
filePattern, outputPattern, destinationType, destinationPath, destHost, destPort, destUser, destPassword, destKeyFile,
|
||||
destBucket, destRegion, destAccessKey, destSecretKey, destEndpoint,
|
||||
destShare, destDomain, destPassiveMode,
|
||||
archivePath, archiveEnabled, rcloneFlags)
|
||||
archivePath, archiveEnabled, deleteAfterTransfer, rcloneFlags)
|
||||
}
|
||||
|
||||
templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
||||
@@ -1303,6 +1306,25 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sm:col-span-6 mt-4">
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="delete_after_transfer"
|
||||
x-model="deleteAfterTransfer"
|
||||
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-secondary-300 dark:border-secondary-700 rounded"/>
|
||||
<input type="hidden" name="delete_after_transfer" :value="deleteAfterTransfer.toString()"/>
|
||||
<label for="delete_after_transfer" class="ml-2 block text-sm text-secondary-700 dark:text-secondary-300">
|
||||
Delete files from source after transfer
|
||||
</label>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-secondary-500 dark:text-secondary-400">
|
||||
<i class="fas fa-exclamation-triangle text-yellow-500 mr-1"></i>
|
||||
Original files will be permanently deleted from source after successful transfer
|
||||
<span x-show="archiveEnabled">and archiving</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="sm:col-span-6">
|
||||
<label for="rclone_flags" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Rclone Flags</label>
|
||||
<div class="relative">
|
||||
|
||||
+205
-4
@@ -6,12 +6,204 @@ import (
|
||||
"github.com/starfleetcptn/gomft/internal/db"
|
||||
)
|
||||
|
||||
// Dialog component for confirmation dialogs
|
||||
templ ConfigDialog(id string, title string, message string, confirmClass string, confirmText string, action string, configID uint, configName string) {
|
||||
<div id={ id } class="hidden fixed inset-0 bg-secondary-900/50 dark:bg-secondary-900/80 backdrop-blur-sm z-50 flex items-center justify-center">
|
||||
<div class="bg-white dark:bg-secondary-800 rounded-lg shadow-xl max-w-md w-full mx-4 overflow-hidden">
|
||||
<div class="px-6 pt-5 pb-3 text-center">
|
||||
<div class="flex justify-center mb-2">
|
||||
<i class="fas fa-exclamation-triangle text-yellow-400 text-3xl"></i>
|
||||
</div>
|
||||
<h3 class="text-xl font-medium text-secondary-900 dark:text-secondary-100">
|
||||
{ title }
|
||||
</h3>
|
||||
</div>
|
||||
<div class="px-6 py-4 text-center">
|
||||
<p class="text-secondary-700 dark:text-secondary-300">
|
||||
{ message }
|
||||
</p>
|
||||
</div>
|
||||
<div class="px-6 py-4 flex justify-end space-x-3">
|
||||
<button type="button" class="btn-secondary" onclick={ hideConfigDialog(id) }>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={ confirmClass }
|
||||
hx-delete={ fmt.Sprintf("/configs/%d", configID) }
|
||||
hx-target="closest li"
|
||||
hx-swap="delete"
|
||||
data-config-name={ configName }
|
||||
data-config-id={ fmt.Sprint(configID) }
|
||||
id={ fmt.Sprintf("delete-config-btn-%d", configID) }
|
||||
onclick={ triggerConfigDelete(id, configID, configName) }>
|
||||
{ confirmText }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
script hideConfigDialog(id string) {
|
||||
document.getElementById(id).classList.add("hidden");
|
||||
}
|
||||
|
||||
script showConfigDialog(id string) {
|
||||
document.getElementById(id).classList.remove("hidden");
|
||||
}
|
||||
|
||||
script triggerConfigDelete(dialogId string, configID uint, configName string) {
|
||||
// Hide the dialog
|
||||
document.getElementById(dialogId).classList.add("hidden");
|
||||
|
||||
// Store data in a way that's accessible to event handlers
|
||||
window.lastDeletedConfig = {
|
||||
id: configID,
|
||||
name: configName
|
||||
};
|
||||
|
||||
// Add custom marker to track this deletion
|
||||
window.currentlyDeletingConfig = true;
|
||||
}
|
||||
|
||||
type ConfigsData struct {
|
||||
Configs []db.TransferConfig
|
||||
}
|
||||
|
||||
templ Configs(ctx context.Context, data ConfigsData) {
|
||||
@LayoutWithContext("Transfer Configurations", ctx) {
|
||||
<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'
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
console.log("Notyf initialized:", window.notyf);
|
||||
}
|
||||
|
||||
// Track all HTMX events for debugging
|
||||
document.addEventListener('htmx:beforeRequest', function(event) {
|
||||
|
||||
// Check if this is a DELETE request by examining the URL and method
|
||||
const path = event.detail.path;
|
||||
const method = event.detail.verb;
|
||||
|
||||
// Pattern match for config deletions (e.g., /configs/123)
|
||||
if (path && method === 'DELETE' && path.match(/^\/configs\/\d+$/)) {
|
||||
|
||||
// This is definitely a delete request - store this information
|
||||
window.isConfigDeleteRequest = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Track HTMX after-request events for config deletion
|
||||
document.addEventListener('htmx:afterRequest', function(event) {
|
||||
|
||||
// Check for config deletion multiple ways
|
||||
const isDeleteRequest =
|
||||
// Check global flag from the triggerConfigDelete function
|
||||
window.currentlyDeletingConfig ||
|
||||
// Check flag from beforeRequest handler
|
||||
window.isConfigDeleteRequest ||
|
||||
// Check URL pattern directly from this event
|
||||
(event.detail.pathInfo && event.detail.pathInfo.requestPath &&
|
||||
event.detail.pathInfo.requestPath.match(/^\/configs\/\d+$/) &&
|
||||
event.detail.verb === 'DELETE');
|
||||
|
||||
|
||||
// If this is a successful delete request, show notification
|
||||
if (isDeleteRequest && event.detail.successful) {
|
||||
|
||||
let configName = "Unknown";
|
||||
|
||||
// Try multiple sources for config name
|
||||
if (event.detail.elt && event.detail.elt.getAttribute) {
|
||||
configName = event.detail.elt.getAttribute('data-config-name') || configName;
|
||||
}
|
||||
|
||||
if (configName === "Unknown" && window.lastDeletedConfig) {
|
||||
// Fallback to our stored config info
|
||||
configName = window.lastDeletedConfig.name;
|
||||
}
|
||||
|
||||
window.notyf.success(`Configuration "${configName}" deleted successfully`);
|
||||
|
||||
// Clear flags
|
||||
window.currentlyDeletingConfig = false;
|
||||
window.isConfigDeleteRequest = false;
|
||||
window.lastDeletedConfig = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Track HTMX error events for config deletion
|
||||
document.addEventListener('htmx:responseError', function(event) {
|
||||
|
||||
// Similar logic as success but for errors
|
||||
const isDeleteRequest =
|
||||
window.currentlyDeletingConfig ||
|
||||
window.isConfigDeleteRequest ||
|
||||
(event.detail.pathInfo && event.detail.pathInfo.requestPath &&
|
||||
event.detail.pathInfo.requestPath.match(/^\/configs\/\d+$/) &&
|
||||
event.detail.verb === 'DELETE');
|
||||
|
||||
if (isDeleteRequest) {
|
||||
|
||||
let configName = "Unknown";
|
||||
|
||||
// Try multiple sources for config name
|
||||
if (event.detail.elt && event.detail.elt.getAttribute) {
|
||||
configName = event.detail.elt.getAttribute('data-config-name') || configName;
|
||||
}
|
||||
|
||||
if (configName === "Unknown" && window.lastDeletedConfig) {
|
||||
// Fallback to our stored config info
|
||||
configName = window.lastDeletedConfig.name;
|
||||
}
|
||||
|
||||
let errorMsg = `Failed to delete configuration "${configName}"`;
|
||||
|
||||
if (event.detail.xhr && event.detail.xhr.responseText) {
|
||||
errorMsg = event.detail.xhr.responseText
|
||||
// error message is a json object
|
||||
const error = JSON.parse(errorMsg);
|
||||
errorMsg = `Error: ${error.error}`;
|
||||
}
|
||||
|
||||
window.notyf.error(errorMsg);
|
||||
|
||||
// Clear flags
|
||||
window.currentlyDeletingConfig = false;
|
||||
window.isConfigDeleteRequest = false;
|
||||
window.lastDeletedConfig = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="py-6">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
@@ -58,11 +250,20 @@ templ Configs(ctx context.Context, data ConfigsData) {
|
||||
<i class="fas fa-edit mr-1"></i>
|
||||
Edit
|
||||
</a>
|
||||
<!-- Add delete dialog for each configuration -->
|
||||
@ConfigDialog(
|
||||
fmt.Sprintf("delete-config-dialog-%d", config.ID),
|
||||
"Delete Configuration",
|
||||
fmt.Sprintf("Are you sure you want to delete the configuration '%s'? This cannot be undone.", config.Name),
|
||||
"btn-danger",
|
||||
"Delete",
|
||||
"delete",
|
||||
config.ID,
|
||||
config.Name,
|
||||
)
|
||||
<button
|
||||
hx-delete={ fmt.Sprintf("/configs/%d", config.ID) }
|
||||
hx-confirm="Are you sure you want to delete this configuration?"
|
||||
hx-target="closest li"
|
||||
hx-swap="outerHTML"
|
||||
type="button"
|
||||
onclick={ showConfigDialog(fmt.Sprintf("delete-config-dialog-%d", config.ID)) }
|
||||
class="btn-danger btn-sm">
|
||||
<i class="fas fa-trash-alt mr-1"></i>
|
||||
Delete
|
||||
|
||||
+256
-5
@@ -6,12 +6,243 @@ import (
|
||||
"github.com/starfleetcptn/gomft/internal/db"
|
||||
)
|
||||
|
||||
// Dialog component for confirmation dialogs - copied from admin_tools.templ
|
||||
templ JobDialog(id string, title string, message string, confirmClass string, confirmText string, action string, jobID uint, jobName string) {
|
||||
<div id={ id } class="hidden fixed inset-0 bg-secondary-900/50 dark:bg-secondary-900/80 backdrop-blur-sm z-50 flex items-center justify-center">
|
||||
<div class="bg-white dark:bg-secondary-800 rounded-lg shadow-xl max-w-md w-full mx-4 overflow-hidden">
|
||||
<div class="px-6 pt-5 pb-3 text-center">
|
||||
<div class="flex justify-center mb-2">
|
||||
<i class="fas fa-exclamation-triangle text-yellow-400 text-3xl"></i>
|
||||
</div>
|
||||
<h3 class="text-xl font-medium text-secondary-900 dark:text-secondary-100">
|
||||
{ title }
|
||||
</h3>
|
||||
</div>
|
||||
<div class="px-6 py-4 text-center">
|
||||
<p class="text-secondary-700 dark:text-secondary-300">
|
||||
{ message }
|
||||
</p>
|
||||
</div>
|
||||
<div class="px-6 py-4 flex justify-end space-x-3">
|
||||
<button type="button" class="btn-secondary" 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>
|
||||
}
|
||||
|
||||
script hideJobDialog(id string) {
|
||||
document.getElementById(id).classList.add("hidden");
|
||||
}
|
||||
|
||||
script showJobDialog(id string) {
|
||||
document.getElementById(id).classList.remove("hidden");
|
||||
}
|
||||
|
||||
script triggerJobDelete(dialogId string, jobID uint, jobName string) {
|
||||
// Hide the dialog
|
||||
document.getElementById(dialogId).classList.add("hidden");
|
||||
|
||||
// Add debugging info
|
||||
console.log(`Job deletion triggered for: ${jobName} (ID: ${jobID})`);
|
||||
|
||||
// Store data in a way that's accessible to event handlers
|
||||
window.lastDeletedJob = {
|
||||
id: jobID,
|
||||
name: jobName
|
||||
};
|
||||
|
||||
// Add custom marker to track this deletion
|
||||
window.currentlyDeletingJob = true;
|
||||
}
|
||||
|
||||
type JobsData struct {
|
||||
Jobs []db.Job
|
||||
}
|
||||
|
||||
templ Jobs(ctx context.Context, data JobsData) {
|
||||
@LayoutWithContext("Transfer Jobs", ctx) {
|
||||
<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);
|
||||
|
||||
// Global function to handle job running
|
||||
window.runJob = function(button) {
|
||||
// Get job data from button attributes
|
||||
const jobId = button.getAttribute('data-job-id');
|
||||
const jobName = button.getAttribute('data-job-name') || `Job #${jobId}`;
|
||||
|
||||
console.log(`Run job clicked for: ${jobName} (ID: ${jobId})`);
|
||||
|
||||
// No loading notification - just listen for the response
|
||||
button.addEventListener('htmx:afterRequest', function(event) {
|
||||
console.log("Run job request completed:", event.detail);
|
||||
if (event.detail.successful) {
|
||||
let displayName = jobName;
|
||||
// Try to extract job name from response headers
|
||||
if (event.detail.headers && event.detail.headers['HX-Job-Name']) {
|
||||
displayName = event.detail.headers['HX-Job-Name'];
|
||||
}
|
||||
console.log(`Showing success notification for job: ${displayName}`);
|
||||
window.notyf.success(`Job "${displayName}" started successfully!`);
|
||||
} 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);
|
||||
}
|
||||
}, { once: true });
|
||||
};
|
||||
|
||||
// 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 job deletions (e.g., /jobs/123)
|
||||
if (path && method === 'DELETE' && path.match(/^\/jobs\/\d+$/)) {
|
||||
console.log("Detected job deletion request via URL pattern");
|
||||
|
||||
// This is definitely a delete request - store this information
|
||||
window.isJobDeleteRequest = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Track HTMX after-request events for job deletion
|
||||
document.addEventListener('htmx:afterRequest', function(event) {
|
||||
console.log("HTMX after request:", event.detail);
|
||||
|
||||
// Check for job deletion multiple ways
|
||||
const isDeleteRequest =
|
||||
// Check global flag from the triggerJobDelete function
|
||||
window.currentlyDeletingJob ||
|
||||
// Check flag from beforeRequest handler
|
||||
window.isJobDeleteRequest ||
|
||||
// Check URL pattern directly from this event
|
||||
(event.detail.pathInfo && event.detail.pathInfo.requestPath &&
|
||||
event.detail.pathInfo.requestPath.match(/^\/jobs\/\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 jobName = "Unknown";
|
||||
|
||||
// Try multiple sources for job name
|
||||
if (event.detail.elt && event.detail.elt.getAttribute) {
|
||||
jobName = event.detail.elt.getAttribute('data-job-name') || jobName;
|
||||
}
|
||||
|
||||
if (jobName === "Unknown" && window.lastDeletedJob) {
|
||||
// Fallback to our stored job info
|
||||
jobName = window.lastDeletedJob.name;
|
||||
}
|
||||
|
||||
console.log(`Showing success notification for deleted job: ${jobName}`);
|
||||
window.notyf.success(`Job "${jobName}" deleted successfully`);
|
||||
|
||||
// Clear flags
|
||||
window.currentlyDeletingJob = false;
|
||||
window.isJobDeleteRequest = false;
|
||||
window.lastDeletedJob = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Track HTMX error events for job deletion
|
||||
document.addEventListener('htmx:responseError', function(event) {
|
||||
console.log("HTMX response error:", event.detail);
|
||||
|
||||
// Similar logic as success but for errors
|
||||
const isDeleteRequest =
|
||||
window.currentlyDeletingJob ||
|
||||
window.isJobDeleteRequest ||
|
||||
(event.detail.pathInfo && event.detail.pathInfo.requestPath &&
|
||||
event.detail.pathInfo.requestPath.match(/^\/jobs\/\d+$/) &&
|
||||
event.detail.verb === 'DELETE');
|
||||
|
||||
if (isDeleteRequest) {
|
||||
console.log("Delete request failed");
|
||||
|
||||
let jobName = "Unknown";
|
||||
|
||||
// Try multiple sources for job name
|
||||
if (event.detail.elt && event.detail.elt.getAttribute) {
|
||||
jobName = event.detail.elt.getAttribute('data-job-name') || jobName;
|
||||
}
|
||||
|
||||
if (jobName === "Unknown" && window.lastDeletedJob) {
|
||||
// Fallback to our stored job info
|
||||
jobName = window.lastDeletedJob.name;
|
||||
}
|
||||
|
||||
let errorMsg = `Failed to delete 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);
|
||||
|
||||
// Clear flags
|
||||
window.currentlyDeletingJob = false;
|
||||
window.isJobDeleteRequest = false;
|
||||
window.lastDeletedJob = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="py-6">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
@@ -71,7 +302,10 @@ templ Jobs(ctx context.Context, data JobsData) {
|
||||
<button
|
||||
hx-post={ fmt.Sprintf("/jobs/%d/run", job.ID) }
|
||||
hx-swap="none"
|
||||
class="btn-primary btn-sm">
|
||||
class="btn-primary btn-sm"
|
||||
data-job-id={ fmt.Sprint(job.ID) }
|
||||
data-job-name={ job.Name }
|
||||
onclick="window.runJob(this)">
|
||||
<i class="fas fa-play mr-1"></i>
|
||||
Run Now
|
||||
</button>
|
||||
@@ -79,11 +313,20 @@ templ Jobs(ctx context.Context, data JobsData) {
|
||||
<i class="fas fa-edit mr-1"></i>
|
||||
Edit
|
||||
</a>
|
||||
<!-- Add delete dialog for each job -->
|
||||
@JobDialog(
|
||||
fmt.Sprintf("delete-job-dialog-%d", job.ID),
|
||||
"Delete Job",
|
||||
fmt.Sprintf("Are you sure you want to delete the job '%s'? This cannot be undone.", determineJobName(job)),
|
||||
"btn-danger",
|
||||
"Delete",
|
||||
"delete",
|
||||
job.ID,
|
||||
determineJobName(job),
|
||||
)
|
||||
<button
|
||||
hx-delete={ fmt.Sprintf("/jobs/%d", job.ID) }
|
||||
hx-confirm="Are you sure you want to delete this job?"
|
||||
hx-target="closest li"
|
||||
hx-swap="outerHTML"
|
||||
type="button"
|
||||
onclick={ showJobDialog(fmt.Sprintf("delete-job-dialog-%d", job.ID)) }
|
||||
class="btn-danger btn-sm">
|
||||
<i class="fas fa-trash-alt mr-1"></i>
|
||||
Delete
|
||||
@@ -135,4 +378,12 @@ templ Jobs(ctx context.Context, data JobsData) {
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to determine the job name (reuse this logic to keep it consistent)
|
||||
func determineJobName(job db.Job) string {
|
||||
if job.Name != "" {
|
||||
return job.Name
|
||||
}
|
||||
return job.Config.Name
|
||||
}
|
||||
@@ -47,6 +47,42 @@ templ LayoutWithContext(title string, ctx context.Context) {
|
||||
<script src="/static/js/app.js"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
|
||||
<link rel="stylesheet" href="/static/css/app.css"/>
|
||||
<!-- Notyf Toast Notifications -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/notyf@3/notyf.min.css" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/notyf@3/notyf.min.js"></script>
|
||||
<script>
|
||||
// Initialize Notyf and make it available globally
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
window.notyfInstance = new Notyf({
|
||||
duration: 3000,
|
||||
position: {
|
||||
x: 'right',
|
||||
y: 'bottom',
|
||||
},
|
||||
types: [
|
||||
{
|
||||
type: 'success',
|
||||
className: 'notyf__toast--success',
|
||||
background: '#10B981',
|
||||
icon: {
|
||||
className: 'fas fa-check-circle',
|
||||
tagName: 'i'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'error',
|
||||
className: 'notyf__toast--error',
|
||||
background: '#EF4444',
|
||||
icon: {
|
||||
className: 'fas fa-exclamation-circle',
|
||||
tagName: 'i'
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
@@ -377,6 +413,9 @@ templ LayoutWithContext(title string, ctx context.Context) {
|
||||
</div>
|
||||
}
|
||||
<main class="flex-grow w-full max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8 animate-fadeIn pb-mobile-nav">
|
||||
<!-- Hidden notification area for HTMX targets -->
|
||||
<div id="notification-area" class="hidden" hx-swap-oob="true"></div>
|
||||
|
||||
{ children... }
|
||||
</main>
|
||||
<footer class="bg-white dark:bg-secondary-800 shadow-inner mt-auto w-full">
|
||||
|
||||
Reference in new issue
Block a user