mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-09-11 00:50:47 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9af8fc051 | ||
|
|
e14d90e37e | ||
|
|
71ec298db5 | ||
|
|
53cf4bc3c5 | ||
|
|
0f9a4cffd2 | ||
|
|
105d223be4 | ||
|
|
7a54a83c8d | ||
|
|
c52267bbb6 | ||
|
|
e93556e537 | ||
|
|
391cfbc50c | ||
|
|
608da9ce1c | ||
|
|
b35174f857 | ||
|
|
ae18cd1a12 | ||
|
|
68dd9fc173 | ||
|
|
f95720758e | ||
|
|
d4c07fdc8e | ||
|
|
28fa65df9d | ||
|
|
c52edb75df | ||
|
|
11fe75dd57 | ||
|
|
ec107ca0ce | ||
|
|
7172d4da90 | ||
|
|
badcbfdb17 | ||
|
|
748d5c0939 | ||
|
|
dd7426a01f | ||
|
|
fde60b7d68 |
@@ -29,6 +29,23 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0 # Needed to get all tags for versioning
|
||||
|
||||
# Set up Node.js for frontend build
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
# Install dependencies
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
# Build frontend assets
|
||||
- name: Build frontend assets
|
||||
run: |
|
||||
node build.js
|
||||
ls -la static/dist/
|
||||
|
||||
# Set version information
|
||||
- name: Set Version
|
||||
id: version
|
||||
|
||||
@@ -30,6 +30,23 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0 # Needed to get all tags for versioning
|
||||
|
||||
# Set up Node.js for frontend build
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
# Install dependencies
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
# Build frontend assets
|
||||
- name: Build frontend assets
|
||||
run: |
|
||||
node build.js
|
||||
ls -la static/dist/
|
||||
|
||||
# Set version information
|
||||
- name: Set Version
|
||||
id: version
|
||||
|
||||
@@ -27,6 +27,26 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build frontend assets
|
||||
run: |
|
||||
# Build JavaScript and CSS assets
|
||||
node build.js
|
||||
|
||||
# Ensure the dist directory exists
|
||||
mkdir -p static/dist
|
||||
|
||||
# Verify the build output
|
||||
ls -la static/dist
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
|
||||
+3
-14
@@ -43,20 +43,7 @@ node_modules/
|
||||
Thumbs.db
|
||||
|
||||
# Ignore all Go files in the components directory
|
||||
components/*.go
|
||||
!components/components.go
|
||||
|
||||
components/providers/*.go
|
||||
!components/providers/providers.go
|
||||
|
||||
components/providers/source/*.go
|
||||
!components/providers/source/source.go
|
||||
|
||||
components/providers/destination/*.go
|
||||
!components/providers/destination/destination.go
|
||||
|
||||
components/providers/common/*.go
|
||||
!components/providers/common/common.go
|
||||
*_templ.go
|
||||
|
||||
# Ignore the data directory
|
||||
data/
|
||||
@@ -79,6 +66,8 @@ backups/
|
||||
/destination/
|
||||
/archive/
|
||||
|
||||
# Ignore the dist directory
|
||||
static/dist/
|
||||
|
||||
# Ignore binaries
|
||||
gomft
|
||||
+44
-3
@@ -1,3 +1,32 @@
|
||||
# Build frontend assets
|
||||
FROM node:20-alpine AS frontend-builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy all files needed for the build first
|
||||
COPY package.json package-lock.json ./
|
||||
COPY build.js ./
|
||||
COPY static/ ./static/
|
||||
COPY tailwind.config.js ./
|
||||
|
||||
# Debug: Show the contents of build.js
|
||||
RUN echo "Contents of build.js:" && cat build.js
|
||||
|
||||
# Remove the build script from postinstall
|
||||
RUN sed -i 's/"postinstall": "npm run build",//' package.json
|
||||
|
||||
# Install dependencies and build with verbose output
|
||||
RUN npm ci && \
|
||||
echo "Building frontend assets..." && \
|
||||
node build.js && \
|
||||
echo "Build complete. Contents of dist:" && \
|
||||
ls -la static/dist/ && \
|
||||
echo "Sample of app.js:" && \
|
||||
head -n 10 static/dist/app.js && \
|
||||
echo "Sample of app.css:" && \
|
||||
head -n 10 static/dist/app.css
|
||||
|
||||
# Go build stage
|
||||
FROM golang:1.24-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
@@ -17,7 +46,20 @@ RUN go install github.com/a-h/templ/cmd/templ@latest
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy the rest of the source code
|
||||
# Create static directory structure
|
||||
RUN mkdir -p /app/static/dist
|
||||
|
||||
# Copy built frontend assets from frontend-builder BEFORE copying Go source
|
||||
COPY --from=frontend-builder /app/static/dist/ /app/static/dist/
|
||||
|
||||
# Copy the rest of the static files
|
||||
COPY static/ /app/static/
|
||||
|
||||
# Verify static files are in place before Go build
|
||||
RUN echo "Verifying static files before Go build:" && \
|
||||
ls -la /app/static/dist/
|
||||
|
||||
# Now copy the rest of the source code
|
||||
COPY . .
|
||||
|
||||
# Generate template files from .templ files
|
||||
@@ -61,8 +103,7 @@ RUN addgroup -g ${GID} ${USERNAME} && \
|
||||
COPY --from=builder /app/gomft /app/
|
||||
COPY --from=builder /usr/local/bin/rclone /usr/local/bin/rclone
|
||||
|
||||
# Copy static files and configurations
|
||||
COPY static/ /app/static/
|
||||
# Copy components
|
||||
COPY components/ /app/components/
|
||||
|
||||
# Copy entrypoint script
|
||||
|
||||
@@ -311,6 +311,10 @@ PGID=1000
|
||||
- If longer than 32 bytes, it will be truncated to 32 bytes
|
||||
- Example: `TOTP_ENCRYPTION_KEY=abcdefghijklmnopqrstuvwxyz123456`
|
||||
|
||||
|
||||
- SSL/TLS Verification Control:
|
||||
- `SKIP_SSL_VERIFY`: Set to `true` to disable SSL/TLS certificate verification for outgoing connections (e.g., webhooks, email). Use with caution, as this can expose connections to man-in-the-middle attacks. Defaults to `false` (verification enabled).
|
||||
- Example: `SKIP_SSL_VERIFY=true`
|
||||
### Logging Configuration
|
||||
|
||||
GoMFT provides configurable logging with rotation support through the following environment variables:
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import * as esbuild from 'esbuild';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { execSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const isWatch = process.argv.includes('--watch');
|
||||
|
||||
// Ensure the dist directory exists
|
||||
const distDir = path.join(__dirname, 'static', 'dist');
|
||||
if (!fs.existsSync(distDir)) {
|
||||
fs.mkdirSync(distDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Copy Font Awesome files
|
||||
const fontAwesomeSrcDir = path.join(__dirname, 'node_modules', '@fortawesome', 'fontawesome-free');
|
||||
const fontAwesomeDestDir = path.join(distDir, 'fontawesome');
|
||||
|
||||
// Copy CSS files
|
||||
const cssFiles = [
|
||||
'css/all.min.css',
|
||||
'css/fontawesome.min.css',
|
||||
'css/solid.min.css',
|
||||
'css/regular.min.css',
|
||||
'css/brands.min.css'
|
||||
];
|
||||
|
||||
cssFiles.forEach(file => {
|
||||
const srcFile = path.join(fontAwesomeSrcDir, file);
|
||||
const destFile = path.join(fontAwesomeDestDir, file);
|
||||
const destDir = path.dirname(destFile);
|
||||
|
||||
if (!fs.existsSync(destDir)) {
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
}
|
||||
|
||||
if (fs.existsSync(srcFile)) {
|
||||
fs.copyFileSync(srcFile, destFile);
|
||||
}
|
||||
});
|
||||
|
||||
// Copy webfonts
|
||||
const webfontsSrcDir = path.join(fontAwesomeSrcDir, 'webfonts');
|
||||
const webfontsDestDir = path.join(fontAwesomeDestDir, 'webfonts');
|
||||
|
||||
if (!fs.existsSync(webfontsDestDir)) {
|
||||
fs.mkdirSync(webfontsDestDir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.readdirSync(webfontsSrcDir).forEach(file => {
|
||||
fs.copyFileSync(
|
||||
path.join(webfontsSrcDir, file),
|
||||
path.join(webfontsDestDir, file)
|
||||
);
|
||||
});
|
||||
|
||||
const commonConfig = {
|
||||
sourcemap: true,
|
||||
minify: true,
|
||||
bundle: true,
|
||||
platform: 'browser',
|
||||
target: ['es2020'],
|
||||
};
|
||||
|
||||
async function buildTailwind() {
|
||||
console.log('Building Tailwind CSS...');
|
||||
execSync('npx tailwindcss -i ./static/css/app.css -o ./static/dist/app.css --minify');
|
||||
}
|
||||
|
||||
async function build() {
|
||||
try {
|
||||
// Build vendor JavaScript bundle (CDN dependencies)
|
||||
await esbuild.build({
|
||||
...commonConfig,
|
||||
entryPoints: ['static/js/vendor.js'],
|
||||
outfile: 'static/dist/vendor.js',
|
||||
format: 'iife',
|
||||
});
|
||||
|
||||
// Build application JavaScript
|
||||
await esbuild.build({
|
||||
...commonConfig,
|
||||
entryPoints: ['static/js/app.js'],
|
||||
outfile: 'static/dist/app.js',
|
||||
format: 'iife',
|
||||
});
|
||||
|
||||
// Build initialization JavaScript
|
||||
await esbuild.build({
|
||||
...commonConfig,
|
||||
entryPoints: ['static/js/init.js'],
|
||||
outfile: 'static/dist/init.js',
|
||||
format: 'iife',
|
||||
});
|
||||
|
||||
// Build CSS with Tailwind
|
||||
await buildTailwind();
|
||||
|
||||
console.log('Build completed successfully!');
|
||||
} catch (error) {
|
||||
console.error('Build failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (isWatch) {
|
||||
// Watch mode
|
||||
console.log('Starting watch mode...');
|
||||
const ctx = await esbuild.context(commonConfig);
|
||||
await ctx.watch();
|
||||
|
||||
// Watch Tailwind CSS
|
||||
execSync('npx tailwindcss -i ./static/css/app.css -o ./static/dist/app.css --watch');
|
||||
|
||||
console.log('Watching for changes...');
|
||||
} else {
|
||||
// Single build
|
||||
build();
|
||||
}
|
||||
@@ -87,7 +87,7 @@ templ AdminRoles(ctx context.Context, data RolesData) {
|
||||
<button
|
||||
data-role-id={ fmt.Sprint(role.ID) }
|
||||
data-role-name={ role.Name }
|
||||
onclick={ showRoleDialog(fmt.Sprintf("delete-role-dialog-%d", role.ID)) }
|
||||
onclick={ showModal(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>
|
||||
@@ -269,7 +269,10 @@ templ AdminRoles(ctx context.Context, data RolesData) {
|
||||
|
||||
// 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 id={ id } tabindex="-1" aria-hidden="true" class="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">
|
||||
<!-- Backdrop -->
|
||||
<div id={ fmt.Sprintf("%s-backdrop", id) } class="fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm"></div>
|
||||
<!-- Modal content -->
|
||||
<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">
|
||||
@@ -288,7 +291,7 @@ templ RoleDialog(id string, title string, message string, confirmClass string, c
|
||||
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">
|
||||
<button type="button" onclick={ closeModal(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>
|
||||
@@ -297,16 +300,6 @@ templ RoleDialog(id string, title string, message string, confirmClass string, c
|
||||
</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");
|
||||
|
||||
@@ -275,7 +275,7 @@ templ formContent(provider *db.AuthProvider, isNew bool) {
|
||||
<!-- Enabled -->
|
||||
<div>
|
||||
<div class="flex items-center">
|
||||
if provider == nil || provider.Enabled {
|
||||
if provider == nil || provider.GetEnabled() {
|
||||
<input
|
||||
type="checkbox"
|
||||
id="enabled"
|
||||
|
||||
@@ -80,7 +80,7 @@ templ AuthProviders(ctx context.Context, providers []db.AuthProvider) {
|
||||
{ string(provider.Type) }
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
if provider.Enabled {
|
||||
if provider.GetEnabled() {
|
||||
<span class="px-2 py-1 text-xs rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
|
||||
Active
|
||||
</span>
|
||||
|
||||
@@ -35,9 +35,9 @@ templ AuthProviderButtons(providers []db.AuthProvider) {
|
||||
} else {
|
||||
<div class="space-y-2 w-full">
|
||||
for _, provider := range providers {
|
||||
if provider.Enabled {
|
||||
<a
|
||||
href={ templ.SafeURL(fmt.Sprintf("/auth/provider/%d", provider.ID)) }
|
||||
if provider.GetEnabled() {
|
||||
<a
|
||||
href={ templ.SafeURL(fmt.Sprintf("/auth/provider/%d", provider.ID)) }
|
||||
class="w-full inline-flex items-center justify-center px-4 py-2.5 bg-gray-100 border border-gray-300 rounded-lg font-medium text-gray-700 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-600"
|
||||
>
|
||||
<span class="flex-shrink-0 w-5 h-5 mr-2.5">
|
||||
|
||||
+105
-32
@@ -12,6 +12,10 @@ import (
|
||||
type ConfigFormData struct {
|
||||
Config *db.TransferConfig
|
||||
IsNew bool
|
||||
// Fields for pre-rendering flags on edit
|
||||
InitialCommand *db.RcloneCommand
|
||||
SelectedFlagsMap map[uint]bool
|
||||
SelectedFlagValues map[uint]string
|
||||
}
|
||||
|
||||
func getConfigFormTitle(isNew bool) string {
|
||||
@@ -27,7 +31,7 @@ func getInitialData(config *db.TransferConfig) string {
|
||||
sourceType := "local"
|
||||
sourcePath := ""
|
||||
sourceHost := ""
|
||||
sourcePort := 22
|
||||
sourcePort := 0 // Initialize to 0 to trigger default setting
|
||||
sourceUser := ""
|
||||
sourcePassword := ""
|
||||
sourceKeyFile := ""
|
||||
@@ -55,7 +59,7 @@ func getInitialData(config *db.TransferConfig) string {
|
||||
destinationType := "local"
|
||||
destinationPath := ""
|
||||
destHost := ""
|
||||
destPort := 22
|
||||
destPort := 0 // Initialize to 0 to trigger default setting
|
||||
destUser := ""
|
||||
destPassword := ""
|
||||
destKeyFile := ""
|
||||
@@ -286,33 +290,34 @@ func getInitialData(config *db.TransferConfig) string {
|
||||
// This will need coordination with your backend to ensure the IDs match the commands
|
||||
let commandName = '';
|
||||
switch(parseInt(this.commandId)) {
|
||||
// Map command IDs to command names - adjust these based on your actual command IDs
|
||||
// Correct mapping based on internal/db/migrations/009_add_rclone_tables.go
|
||||
case 1: commandName = 'copy'; break;
|
||||
case 2: commandName = 'move'; break;
|
||||
case 3: commandName = 'sync'; break;
|
||||
case 4: commandName = 'ls'; break;
|
||||
case 5: commandName = 'lsd'; break;
|
||||
case 6: commandName = 'lsl'; break;
|
||||
case 7: commandName = 'lsf'; break;
|
||||
case 8: commandName = 'lsjson'; break;
|
||||
case 9: commandName = 'md5sum'; break;
|
||||
case 10: commandName = 'sha1sum'; break;
|
||||
case 11: commandName = 'size'; break;
|
||||
case 12: commandName = 'delete'; break;
|
||||
case 13: commandName = 'purge'; break;
|
||||
case 14: commandName = 'mkdir'; break;
|
||||
case 15: commandName = 'rmdir'; break;
|
||||
case 16: commandName = 'rmdirs'; break;
|
||||
case 17: commandName = 'check'; break;
|
||||
case 18: commandName = 'cleanup'; break;
|
||||
case 19: commandName = 'dedupe'; break;
|
||||
case 20: commandName = 'version'; break;
|
||||
case 21: commandName = 'listremotes'; break;
|
||||
case 22: commandName = 'cryptcheck'; break;
|
||||
case 23: commandName = 'bisync'; break;
|
||||
case 24: commandName = 'copyto'; break;
|
||||
case 25: commandName = 'moveto'; break;
|
||||
default: commandName = 'copy'; // Default to copy
|
||||
case 2: commandName = 'sync'; break;
|
||||
case 3: commandName = 'bisync'; break;
|
||||
case 4: commandName = 'move'; break;
|
||||
case 5: commandName = 'delete'; break;
|
||||
case 6: commandName = 'purge'; break;
|
||||
case 7: commandName = 'mkdir'; break;
|
||||
case 8: commandName = 'rmdir'; break;
|
||||
case 9: commandName = 'rmdirs'; break;
|
||||
case 10: commandName = 'check'; break;
|
||||
case 11: commandName = 'ls'; break;
|
||||
case 12: commandName = 'lsd'; break;
|
||||
case 13: commandName = 'lsl'; break;
|
||||
case 14: commandName = 'lsf'; break;
|
||||
case 15: commandName = 'lsjson'; break;
|
||||
case 16: commandName = 'md5sum'; break;
|
||||
case 17: commandName = 'sha1sum'; break;
|
||||
case 18: commandName = 'size'; break;
|
||||
case 19: commandName = 'version'; break;
|
||||
case 20: commandName = 'cleanup'; break;
|
||||
case 21: commandName = 'dedupe'; break;
|
||||
case 22: commandName = 'copyto'; break;
|
||||
case 23: commandName = 'moveto'; break;
|
||||
case 24: commandName = 'listremotes'; break;
|
||||
case 25: commandName = 'obscure'; break;
|
||||
case 26: commandName = 'cryptcheck'; break;
|
||||
default: commandName = 'copy'; // Default to copy if ID is unknown
|
||||
}
|
||||
|
||||
console.log('Command ID:', this.commandId, 'Command Name:', commandName);
|
||||
@@ -386,11 +391,41 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
||||
// Ensure initial form state displays correctly on load
|
||||
sourceType = sourceType || 'local';
|
||||
destinationType = destinationType || 'local';
|
||||
sourcePort = sourcePort || 22;
|
||||
destPort = destPort || 22;
|
||||
|
||||
// Set default ports based on connection type
|
||||
if (sourcePort === 0 || !sourcePort) {
|
||||
if (sourceType === 'sftp') {
|
||||
sourcePort = 22;
|
||||
} else if (sourceType === 'ftp') {
|
||||
sourcePort = 21;
|
||||
}
|
||||
}
|
||||
|
||||
if (destPort === 0 || !destPort) {
|
||||
if (destinationType === 'sftp') {
|
||||
destPort = 22;
|
||||
} else if (destinationType === 'ftp') {
|
||||
destPort = 21;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize command requirements
|
||||
updateCommandRequirements();
|
||||
})"
|
||||
x-effect="if (sourceType === 'sftp' && (sourcePort === 0 || sourcePort === 21)) {
|
||||
sourcePort = 22;
|
||||
console.log('Updating source port to 22 for SFTP');
|
||||
} else if (sourceType === 'ftp' && (sourcePort === 0 || sourcePort === 22)) {
|
||||
sourcePort = 21;
|
||||
console.log('Updating source port to 21 for FTP');
|
||||
}"
|
||||
x-effect="if (destinationType === 'sftp' && (destPort === 0 || destPort === 21)) {
|
||||
destPort = 22;
|
||||
console.log('Updating destination port to 22 for SFTP');
|
||||
} else if (destinationType === 'ftp' && (destPort === 0 || destPort === 22)) {
|
||||
destPort = 21;
|
||||
console.log('Updating destination port to 21 for FTP');
|
||||
}"
|
||||
>
|
||||
|
||||
<!-- Configuration Details Section -->
|
||||
@@ -425,8 +460,18 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
||||
<i class="fas fa-terminal mr-2 text-blue-500 dark:text-blue-400"></i>Command Configuration
|
||||
</h3>
|
||||
|
||||
<!-- Use the RcloneFlags component from common package -->
|
||||
@common.RcloneFlags()
|
||||
<!-- Additonal Rclone Flags -->
|
||||
@common.RcloneFlags(data.Config.CommandID) // Pass current command ID
|
||||
|
||||
<!-- Container for flags, pre-rendered on edit, loaded via HTMX on new/change -->
|
||||
<div id="command-flags-container" class="mt-4">
|
||||
if !data.IsNew && data.InitialCommand != nil {
|
||||
// Pre-render flags if editing and command data is available
|
||||
@common.RcloneCommandFlagsContent(data.InitialCommand, data.SelectedFlagsMap, data.SelectedFlagValues)
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Source Configuration Section -->
|
||||
@@ -438,6 +483,20 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
||||
<!-- Source selection -->
|
||||
@common.SourceSelection()
|
||||
|
||||
<div class="mt-4">
|
||||
<button type="button"
|
||||
class="text-white bg-green-600 hover:bg-green-700 focus:ring-4 focus:ring-green-300 font-medium rounded-lg text-sm px-4 py-2 text-center dark:bg-green-500 dark:hover:bg-green-600 dark:focus:ring-green-800"
|
||||
hx-post="/configs/test-connection"
|
||||
hx-include="closest form"
|
||||
hx-vals='{"providerType": "source"}'
|
||||
hx-swap="none"
|
||||
hx-indicator="#source-test-spinner">
|
||||
<i class="fas fa-plug mr-1"></i> Test Source
|
||||
<span id="source-test-spinner" class="htmx-indicator ml-2"><i class="fas fa-spinner fa-spin"></i></span>
|
||||
</button>
|
||||
<!-- Removed target div, result shown via toast -->
|
||||
</div>
|
||||
|
||||
<!-- Source type specific forms -->
|
||||
<template x-if="sourceType === 'local'">
|
||||
@source.LocalSourceForm()
|
||||
@@ -499,6 +558,20 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
||||
<!-- Destination selection -->
|
||||
@common.DestinationSelection()
|
||||
|
||||
<div class="mt-4">
|
||||
<button type="button"
|
||||
class="text-white bg-green-600 hover:bg-green-700 focus:ring-4 focus:ring-green-300 font-medium rounded-lg text-sm px-4 py-2 text-center dark:bg-green-500 dark:hover:bg-green-600 dark:focus:ring-green-800"
|
||||
hx-post="/configs/test-connection"
|
||||
hx-include="closest form"
|
||||
hx-vals='{"providerType": "destination"}'
|
||||
hx-swap="none"
|
||||
hx-indicator="#dest-test-spinner">
|
||||
<i class="fas fa-plug mr-1"></i> Test Destination
|
||||
<span id="dest-test-spinner" class="htmx-indicator ml-2"><i class="fas fa-spinner fa-spin"></i></span>
|
||||
</button>
|
||||
<!-- Removed target div, result shown via toast -->
|
||||
</div>
|
||||
|
||||
<!-- Destination type specific forms -->
|
||||
<template x-if="destinationType === 'local'">
|
||||
@destination.LocalDestinationForm()
|
||||
|
||||
@@ -8,7 +8,10 @@ import (
|
||||
|
||||
// Dialog component for confirmation dialogs using Flowbite modal
|
||||
templ ConfigDialog(id string, title string, message string, confirmClass string, confirmText string, action string, configID uint, configName 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 id={ id } tabindex="-1" aria-hidden="true" class="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">
|
||||
<!-- Backdrop -->
|
||||
<div id={ fmt.Sprintf("%s-backdrop", id) } class="fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm"></div>
|
||||
<!-- Modal content -->
|
||||
<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">
|
||||
@@ -30,7 +33,7 @@ templ ConfigDialog(id string, title string, message string, confirmClass string,
|
||||
onclick={ triggerConfigDelete(id, configID, configName) }>
|
||||
{ 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">
|
||||
<button type="button" onclick={ closeModal(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>
|
||||
@@ -39,14 +42,26 @@ templ ConfigDialog(id string, title string, message string, confirmClass string,
|
||||
</div>
|
||||
}
|
||||
|
||||
script hideConfigDialog(id string) {
|
||||
document.getElementById(id).classList.add("hidden");
|
||||
document.getElementById(id).classList.remove("flex");
|
||||
script closeModal(id string) {
|
||||
const modal = document.getElementById(id);
|
||||
const backdrop = document.getElementById(id + '-backdrop');
|
||||
if (modal) {
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
}
|
||||
if (backdrop) {
|
||||
backdrop.remove();
|
||||
}
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
script showConfigDialog(id string) {
|
||||
document.getElementById(id).classList.remove("hidden");
|
||||
document.getElementById(id).classList.add("flex");
|
||||
script showModal(id string) {
|
||||
const modal = document.getElementById(id);
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
script triggerConfigDelete(dialogId string, configID uint, configName string) {
|
||||
@@ -429,7 +444,7 @@ templ Configs(ctx context.Context, data ConfigsData) {
|
||||
)
|
||||
<button
|
||||
type="button"
|
||||
onclick={ showConfigDialog(fmt.Sprintf("delete-config-dialog-%d", config.ID)) }
|
||||
onclick={ showModal(fmt.Sprintf("delete-config-dialog-%d", config.ID)) }
|
||||
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">
|
||||
<i class="fas fa-trash-alt w-3.5 h-3.5 mr-1.5"></i>
|
||||
Delete
|
||||
|
||||
@@ -76,7 +76,10 @@ 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) {
|
||||
<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 id={ id } tabindex="-1" aria-hidden="true" class="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">
|
||||
<!-- Backdrop -->
|
||||
<div id={ fmt.Sprintf("%s-backdrop", id) } class="fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm"></div>
|
||||
<!-- Modal content -->
|
||||
<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">
|
||||
@@ -112,7 +115,7 @@ templ FileMetadataDialog(id string, title string, message string, confirmClass s
|
||||
{ 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">
|
||||
<button type="button" onclick={ closeModal(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>
|
||||
@@ -121,16 +124,6 @@ templ FileMetadataDialog(id string, title string, message string, confirmClass s
|
||||
</div>
|
||||
}
|
||||
|
||||
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");
|
||||
@@ -211,7 +204,7 @@ templ FileMetadataListPartial(data FileMetadataListData) {
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
onclick={ showFileDialog(fmt.Sprintf("delete-file-dialog-%d", file.ID)) }
|
||||
onclick={ showModal(fmt.Sprintf("delete-file-dialog-%d", file.ID)) }
|
||||
class="font-medium text-red-600 dark:text-red-500 hover:underline">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
@@ -392,9 +385,7 @@ templ FileMetadataList(ctx context.Context, data FileMetadataListData) {
|
||||
});
|
||||
|
||||
// Track all HTMX events for debugging
|
||||
document.addEventListener('htmx:beforeRequest', function(event) {
|
||||
console.log("HTMX before request:", event.detail);
|
||||
|
||||
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;
|
||||
@@ -411,9 +402,7 @@ templ FileMetadataList(ctx context.Context, data FileMetadataListData) {
|
||||
});
|
||||
|
||||
// Track HTMX after-request events for file deletion
|
||||
document.addEventListener('htmx:afterRequest', function(event) {
|
||||
console.log("HTMX after request:", event.detail);
|
||||
|
||||
document.addEventListener('htmx:afterRequest', function(event) {
|
||||
// Check for file deletion multiple ways
|
||||
const isDeleteRequest =
|
||||
// Check global flag from the triggerFileDelete function
|
||||
@@ -970,7 +959,7 @@ templ FileMetadataDetails(ctx context.Context, data FileMetadataDetailsData) {
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
onclick={ showFileDialog(fmt.Sprintf("delete-file-dialog-%d", data.File.ID)) }
|
||||
onclick={ showModal(fmt.Sprintf("delete-file-dialog-%d", data.File.ID)) }
|
||||
class="text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800">
|
||||
<i class="fas fa-trash mr-2"></i> Delete Record
|
||||
</button>
|
||||
@@ -1336,7 +1325,7 @@ templ FileMetadataSearchContent(data FileMetadataSearchData) {
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
onclick={ showFileDialog(fmt.Sprintf("delete-file-dialog-%d", file.ID)) }
|
||||
onclick={ showModal(fmt.Sprintf("delete-file-dialog-%d", file.ID)) }
|
||||
class="font-medium text-red-600 dark:text-red-500 hover:underline">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
package details
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"github.com/starfleetcptn/gomft/components"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata/dialog" // Import dialog package
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata/utils" // Import utils package
|
||||
)
|
||||
|
||||
// FileMetadataDetails renders the details view for a file metadata, matching original structure
|
||||
templ FileMetadataDetails(ctx context.Context, data file_metadata.FileMetadataDetailsData) {
|
||||
@components.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>
|
||||
|
||||
@utils.FileMetadataJS() // Include JS for toasts, etc.
|
||||
|
||||
<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">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||
<i class="fas fa-file-alt w-6 h-6 mr-2 text-blue-500"></i>
|
||||
File Details: { data.File.FileName }
|
||||
</h1>
|
||||
<a href="/files" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||
<i class="fas fa-arrow-left mr-2"></i> Back to Files
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full">
|
||||
<!-- Card header -->
|
||||
<div class="p-4 md:p-5 border-b border-gray-200 dark:border-gray-700">
|
||||
<h5 class="text-xl font-bold leading-none text-gray-900 dark:text-white">
|
||||
{ data.File.FileName }
|
||||
</h5>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
File ID: { strconv.FormatUint(uint64(data.File.ID), 10) }
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Card content -->
|
||||
<div class="p-4 md:p-5">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- File Information -->
|
||||
<div>
|
||||
<h6 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white flex items-center">
|
||||
<i class="fas fa-file-alt mr-2 text-gray-500 dark:text-gray-400"></i> File Information
|
||||
</h6>
|
||||
<div class="overflow-x-auto relative shadow-md sm:rounded-lg">
|
||||
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
|
||||
<tbody>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Filename
|
||||
</th>
|
||||
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||
{ data.File.FileName }
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Size
|
||||
</th>
|
||||
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||
{ utils.FormatFileSize(data.File.FileSize) }
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Hash
|
||||
</th>
|
||||
<td class="py-3 px-4 break-all bg-white dark:bg-gray-800">
|
||||
if data.File.FileHash != "" {
|
||||
<span class="font-mono">{ data.File.FileHash }</span>
|
||||
} else {
|
||||
<span class="text-gray-400 dark:text-gray-500 italic">Not available</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Status
|
||||
</th>
|
||||
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||
<span class={ "text-xs font-medium px-2.5 py-0.5 rounded", utils.GetStatusBadgeClass(data.File.Status) }>
|
||||
{ data.File.Status }
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Original Path
|
||||
</th>
|
||||
<td class="py-3 px-4 break-all bg-white dark:bg-gray-800">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-folder mr-2 text-yellow-500"></i>
|
||||
<span>{ data.File.OriginalPath }</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Destination Path
|
||||
</th>
|
||||
<td class="py-3 px-4 break-all bg-white dark:bg-gray-800">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-folder-open mr-2 text-blue-500"></i>
|
||||
<span>{ data.File.DestinationPath }</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Processing Information -->
|
||||
<div>
|
||||
<h6 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white flex items-center">
|
||||
<i class="fas fa-cogs mr-2 text-gray-500 dark:text-gray-400"></i> Processing Information
|
||||
</h6>
|
||||
<div class="overflow-x-auto relative shadow-md sm:rounded-lg">
|
||||
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
|
||||
<tbody>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Job
|
||||
</th>
|
||||
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||
<a href={ templ.SafeURL(fmt.Sprintf("/files/job/%d", data.File.JobID)) } class="font-medium text-blue-600 dark:text-blue-500 hover:underline">
|
||||
{ data.File.Job.Name }
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Processed Time
|
||||
</th>
|
||||
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||
<div class="flex items-center">
|
||||
<i class="far fa-clock mr-2 text-gray-500"></i>
|
||||
<span>{ data.File.ProcessedTime.Format("2006-01-02 15:04:05") }</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Creation Time
|
||||
</th>
|
||||
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-calendar-plus mr-2 text-green-500"></i>
|
||||
<span>{ data.File.CreationTime.Format("2006-01-02 15:04:05") }</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Modification Time
|
||||
</th>
|
||||
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-calendar-alt mr-2 text-purple-500"></i>
|
||||
<span>{ data.File.ModTime.Format("2006-01-02 15:04:05") }</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
if data.File.Status == "error" && data.File.ErrorMessage != "" {
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Error
|
||||
</th>
|
||||
<td class="py-3 px-4 break-all bg-white dark:bg-gray-800">
|
||||
<div class="flex items-start">
|
||||
<i class="fas fa-exclamation-triangle mt-1 mr-2 text-red-500"></i>
|
||||
<span class="text-red-600 dark:text-red-400">{ data.File.ErrorMessage }</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Record Created
|
||||
</th>
|
||||
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||
{ data.File.CreatedAt.Format("2006-01-02 15:04:05") }
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Record Updated
|
||||
</th>
|
||||
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||
{ data.File.UpdatedAt.Format("2006-01-02 15:04:05") }
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete dialog component call -->
|
||||
@dialog.FileMetadataDialog(
|
||||
fmt.Sprintf("delete-file-dialog-%d", data.File.ID),
|
||||
"Delete File Metadata",
|
||||
fmt.Sprintf("Are you sure you want to delete the metadata for '%s'? This cannot be undone.", data.File.FileName),
|
||||
"text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800", // Use correct classes
|
||||
"Delete",
|
||||
"delete",
|
||||
data.File.ID,
|
||||
data.File.FileName,
|
||||
"details", // Indicate this is from the details view
|
||||
)
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="mt-6 flex flex-wrap justify-end gap-3">
|
||||
<a href="/files" 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">
|
||||
<i class="fas fa-list mr-2"></i> Back to Files
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
onclick={ templ.ComponentScript{Call: fmt.Sprintf("showModal('delete-file-dialog-%d')", data.File.ID)} }
|
||||
class="text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800">
|
||||
<i class="fas fa-trash mr-2"></i> Delete Record
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Set dark background color if in dark mode
|
||||
if (document.documentElement.classList.contains('dark')) {
|
||||
document.getElementById('file-details-container').style.backgroundColor = '#111827';
|
||||
}
|
||||
|
||||
// Add event listener for theme changes
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener('click', function() {
|
||||
setTimeout(function() {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
document.getElementById('file-details-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package details
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/starfleetcptn/gomft/components"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata/dialog" // Import dialog package
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata/utils" // Import utils package
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// FileMetadataDetails renders the details view for a file metadata, matching original structure
|
||||
func FileMetadataDetails(ctx context.Context, data file_metadata.FileMetadataDetailsData) 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!-- Status and Error Messages --> <div id=\"toast-container\" class=\"fixed top-5 right-5 z-50 flex flex-col gap-2\"></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = utils.FileMetadataJS().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " <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\"><h1 class=\"text-2xl font-bold text-gray-900 dark:text-white flex items-center\"><i class=\"fas fa-file-alt w-6 h-6 mr-2 text-blue-500\"></i> File Details: ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.FileName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 26, Col: 40}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</h1><a href=\"/files\" class=\"text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-arrow-left mr-2\"></i> Back to Files</a></div><div class=\"bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full\"><!-- Card header --><div class=\"p-4 md:p-5 border-b border-gray-200 dark:border-gray-700\"><h5 class=\"text-xl font-bold leading-none text-gray-900 dark:text-white\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.FileName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 37, Col: 27}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</h5><p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">File ID: ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.FormatUint(uint64(data.File.ID), 10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 40, Col: 62}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</p></div><!-- Card content --><div class=\"p-4 md:p-5\"><div class=\"grid grid-cols-1 md:grid-cols-2 gap-6\"><!-- File Information --><div><h6 class=\"text-lg font-semibold mb-4 text-gray-900 dark:text-white flex items-center\"><i class=\"fas fa-file-alt mr-2 text-gray-500 dark:text-gray-400\"></i> File Information</h6><div class=\"overflow-x-auto relative shadow-md sm:rounded-lg\"><table class=\"w-full text-sm text-left text-gray-500 dark:text-gray-400\"><tbody><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Filename</th><td class=\"py-3 px-4 bg-white dark:bg-gray-800\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.FileName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 60, Col: 33}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</td></tr><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Size</th><td class=\"py-3 px-4 bg-white dark:bg-gray-800\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(utils.FormatFileSize(data.File.FileSize))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 68, Col: 55}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</td></tr><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Hash</th><td class=\"py-3 px-4 break-all bg-white dark:bg-gray-800\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.File.FileHash != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<span class=\"font-mono\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.FileHash)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 77, Col: 58}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<span class=\"text-gray-400 dark:text-gray-500 italic\">Not available</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</td></tr><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Status</th><td class=\"py-3 px-4 bg-white dark:bg-gray-800\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 = []any{"text-xs font-medium px-2.5 py-0.5 rounded", utils.GetStatusBadgeClass(data.File.Status)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<span class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var9).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.Status)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 89, Col: 32}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</span></td></tr><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Original Path</th><td class=\"py-3 px-4 break-all bg-white dark:bg-gray-800\"><div class=\"flex items-center\"><i class=\"fas fa-folder mr-2 text-yellow-500\"></i> <span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.OriginalPath)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 100, Col: 44}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</span></div></td></tr><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Destination Path</th><td class=\"py-3 px-4 break-all bg-white dark:bg-gray-800\"><div class=\"flex items-center\"><i class=\"fas fa-folder-open mr-2 text-blue-500\"></i> <span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.DestinationPath)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 111, Col: 47}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</span></div></td></tr></tbody></table></div></div><!-- Processing Information --><div><h6 class=\"text-lg font-semibold mb-4 text-gray-900 dark:text-white flex items-center\"><i class=\"fas fa-cogs mr-2 text-gray-500 dark:text-gray-400\"></i> Processing Information</h6><div class=\"overflow-x-auto relative shadow-md sm:rounded-lg\"><table class=\"w-full text-sm text-left text-gray-500 dark:text-gray-400\"><tbody><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Job</th><td class=\"py-3 px-4 bg-white dark:bg-gray-800\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/files/job/%d", data.File.JobID))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var14)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" class=\"font-medium text-blue-600 dark:text-blue-500 hover:underline\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.Job.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 134, Col: 34}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</a></td></tr><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Processed Time</th><td class=\"py-3 px-4 bg-white dark:bg-gray-800\"><div class=\"flex items-center\"><i class=\"far fa-clock mr-2 text-gray-500\"></i> <span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.ProcessedTime.Format("2006-01-02 15:04:05"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 145, Col: 75}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</span></div></td></tr><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Creation Time</th><td class=\"py-3 px-4 bg-white dark:bg-gray-800\"><div class=\"flex items-center\"><i class=\"fas fa-calendar-plus mr-2 text-green-500\"></i> <span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.CreationTime.Format("2006-01-02 15:04:05"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 156, Col: 74}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</span></div></td></tr><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Modification Time</th><td class=\"py-3 px-4 bg-white dark:bg-gray-800\"><div class=\"flex items-center\"><i class=\"fas fa-calendar-alt mr-2 text-purple-500\"></i> <span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.ModTime.Format("2006-01-02 15:04:05"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 167, Col: 69}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</span></div></td></tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.File.Status == "error" && data.File.ErrorMessage != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Error</th><td class=\"py-3 px-4 break-all bg-white dark:bg-gray-800\"><div class=\"flex items-start\"><i class=\"fas fa-exclamation-triangle mt-1 mr-2 text-red-500\"></i> <span class=\"text-red-600 dark:text-red-400\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.ErrorMessage)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 179, Col: 84}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</span></div></td></tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Record Created</th><td class=\"py-3 px-4 bg-white dark:bg-gray-800\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var20 string
|
||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.CreatedAt.Format("2006-01-02 15:04:05"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 189, Col: 64}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</td></tr><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Record Updated</th><td class=\"py-3 px-4 bg-white dark:bg-gray-800\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.UpdatedAt.Format("2006-01-02 15:04:05"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 197, Col: 64}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</td></tr></tbody></table></div></div></div><!-- Delete dialog component call -->")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = dialog.FileMetadataDialog(
|
||||
fmt.Sprintf("delete-file-dialog-%d", data.File.ID),
|
||||
"Delete File Metadata",
|
||||
fmt.Sprintf("Are you sure you want to delete the metadata for '%s'? This cannot be undone.", data.File.FileName),
|
||||
"text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800", // Use correct classes
|
||||
"Delete",
|
||||
"delete",
|
||||
data.File.ID,
|
||||
data.File.FileName,
|
||||
"details", // Indicate this is from the details view
|
||||
).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<!-- Action buttons --><div class=\"mt-6 flex flex-wrap justify-end gap-3\"><a href=\"/files\" 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\"><i class=\"fas fa-list mr-2\"></i> Back to Files</a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("showModal('delete-file-dialog-%d')", data.File.ID)})
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<button type=\"button\" onclick=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var22 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("showModal('delete-file-dialog-%d')", data.File.ID)}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22.Call)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" class=\"text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800\"><i class=\"fas fa-trash mr-2\"></i> Delete Record</button></div></div></div></div><script>\n\t\t\t\t// Set dark background color if in dark mode\n\t\t\t\tif (document.documentElement.classList.contains('dark')) {\n\t\t\t\t\tdocument.getElementById('file-details-container').style.backgroundColor = '#111827';\n\t\t\t\t}\n\n\t\t\t\t// Add event listener for theme changes\n\t\t\t\tdocument.addEventListener('DOMContentLoaded', function() {\n\t\t\t\t\tconst themeToggle = document.getElementById('theme-toggle');\n\t\t\t\t\tif (themeToggle) {\n\t\t\t\t\t\tthemeToggle.addEventListener('click', function() {\n\t\t\t\t\t\t\tsetTimeout(function() {\n\t\t\t\t\t\t\t\tconst isDark = document.documentElement.classList.contains('dark');\n\t\t\t\t\t\t\t\tdocument.getElementById('file-details-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';\n\t\t\t\t\t\t\t}, 50);\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t</script></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = components.LayoutWithContext("File Details", ctx).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,57 @@
|
||||
package dialog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata/utils"
|
||||
)
|
||||
|
||||
// FileMetadataDialog renders a confirmation dialog for file metadata actions
|
||||
templ FileMetadataDialog(id string, title string, message string, confirmClass string, confirmText string, action string, fileID uint, fileName string, section string) {
|
||||
@utils.FileMetadataJS()
|
||||
<div id={ id } tabindex="-1" aria-hidden="true" class="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">
|
||||
<!-- Backdrop -->
|
||||
<div id={ fmt.Sprintf("%s-backdrop", id) } class="fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm"></div>
|
||||
<!-- Modal content -->
|
||||
<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>
|
||||
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={ templ.ComponentScript{Call: fmt.Sprintf("triggerFileDelete('%s', %d, '%s')", 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={ templ.ComponentScript{Call: fmt.Sprintf("triggerFileDelete('%s', %d, '%s')", id, fileID, fileName)} }>
|
||||
{ confirmText }
|
||||
</button>
|
||||
}
|
||||
<button type="button" onclick={ templ.ComponentScript{Call: fmt.Sprintf("closeModal('%s')", 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>
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package dialog
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata/utils"
|
||||
)
|
||||
|
||||
// FileMetadataDialog renders a confirmation dialog for file metadata actions
|
||||
func FileMetadataDialog(id string, title string, message string, confirmClass string, confirmText string, action string, fileID uint, fileName string, section string) 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = utils.FileMetadataJS().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(id)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 11, Col: 13}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" tabindex=\"-1\" aria-hidden=\"true\" class=\"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\"><!-- Backdrop --><div id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s-backdrop", id))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 13, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" class=\"fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm\"></div><!-- Modal content --><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 templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if action == "delete" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<i class=\"fas fa-trash-alt text-red-400 text-3xl mb-4\"></i>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<i class=\"fas fa-exclamation-triangle text-yellow-400 text-3xl mb-4\"></i>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<h3 class=\"mb-5 text-lg font-normal text-gray-500 dark:text-gray-400\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(message)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 23, Col: 84}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</h3>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if section == "list" {
|
||||
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("triggerFileDelete('%s', %d, '%s')", id, fileID, fileName)})
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<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=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("/files/%d", fileID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 28, Col: 51}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" hx-target=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("#file-row-%d", fileID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 29, Col: 54}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" hx-swap=\"delete\" data-file-name=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fileName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 31, Col: 32}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" data-file-id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(fileID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 32, Col: 40}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("delete-file-btn-%d", fileID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 33, Col: 53}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" onclick=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("triggerFileDelete('%s', %d, '%s')", id, fileID, fileName)}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10.Call)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(confirmText)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 35, Col: 20}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</button> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("triggerFileDelete('%s', %d, '%s')", id, fileID, fileName)})
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<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=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("/files/%d", fileID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 41, Col: 51}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" hx-redirect=\"/files\" data-file-name=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fileName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 43, Col: 32}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" data-file-id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(fileID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 44, Col: 40}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("delete-file-btn-%d", fileID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 45, Col: 53}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" onclick=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var16 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("triggerFileDelete('%s', %d, '%s')", id, fileID, fileName)}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16.Call)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(confirmText)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 47, Col: 20}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</button> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("closeModal('%s')", id)})
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<button type=\"button\" onclick=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("closeModal('%s')", id)}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18.Call)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" 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>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,116 @@
|
||||
package list
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata/utils"
|
||||
"github.com/starfleetcptn/gomft/components" // Import the main components package
|
||||
)
|
||||
|
||||
// FileMetadataList renders the list of file metadata
|
||||
templ FileMetadataList(ctx context.Context, data file_metadata.FileMetadataListData) {
|
||||
@components.LayoutWithContext("Files", ctx) { // Call using components package
|
||||
<!-- Status and Error Messages -->
|
||||
<div id="toast-container" class="fixed top-5 right-5 z-50 flex flex-col gap-2"></div>
|
||||
|
||||
@utils.FileMetadataJS() // Use capitalized function name
|
||||
|
||||
<div id="list-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">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||
<i class="fas fa-file-alt w-6 h-6 mr-2 text-blue-500"></i>
|
||||
Files
|
||||
</h1>
|
||||
<div class="flex gap-3">
|
||||
<a href="/files/search" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||
<i class="fas fa-search mr-2"></i> Advanced Search
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter Form -->
|
||||
<div class="p-4 mb-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full">
|
||||
<h5 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">Filter Files</h5>
|
||||
<form
|
||||
hx-get="/files/partial"
|
||||
hx-target="#file-list-container"
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#filter-loading"
|
||||
hx-headers='{"X-HX-Request": "true"}'
|
||||
class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
if data.Job == nil {
|
||||
<div>
|
||||
<label for="job_id" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Job</label>
|
||||
<input type="text" id="job_id" name="job_id" value={ data.Filter.JobID } placeholder="Job ID" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"/>
|
||||
</div>
|
||||
}
|
||||
<div>
|
||||
<label for="status" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Status</label>
|
||||
<select id="status" name="status" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="processed" selected?={ data.Filter.Status == "processed" }>Processed</option>
|
||||
<option value="archived" selected?={ data.Filter.Status == "archived" }>Archived</option>
|
||||
<option value="deleted" selected?={ data.Filter.Status == "deleted" }>Deleted</option>
|
||||
<option value="archived_and_deleted" selected?={ data.Filter.Status == "archived_and_deleted" }>Archived & Deleted</option>
|
||||
<option value="error" selected?={ data.Filter.Status == "error" }>Error</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="filename" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Filename</label>
|
||||
<input type="text" id="filename" name="filename" value={ data.Filter.FileName } placeholder="Filename or partial match"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||
</div>
|
||||
<div class="md:col-span-3 flex justify-end items-center">
|
||||
<button type="submit" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||
<i class="fas fa-filter mr-2"></i> Apply Filters
|
||||
</button>
|
||||
<div id="filter-loading" class="htmx-indicator ml-2 flex items-center">
|
||||
<i class="fas fa-circle-notch fa-spin text-blue-600"></i>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full">
|
||||
<!-- Card header -->
|
||||
<div class="p-4 md:p-5 border-b border-gray-200 dark:border-gray-700">
|
||||
<h5 class="text-xl font-bold leading-none text-gray-900 dark:text-white">
|
||||
File List
|
||||
</h5>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Showing { strconv.FormatInt(int64((data.Page - 1) * data.Limit + 1), 10) } to { strconv.FormatInt(int64(min(data.Page * data.Limit, int(data.TotalCount))), 10) } of { strconv.FormatInt(data.TotalCount, 10) } files
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Card content -->
|
||||
<div id="file-list-container" class="p-4 md:p-5">
|
||||
@FileMetadataListPartial(ctx, data, "/files/partial", "#file-list-container")
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Set dark background color if in dark mode
|
||||
if (document.documentElement.classList.contains('dark')) {
|
||||
document.getElementById('list-container').style.backgroundColor = '#111827';
|
||||
}
|
||||
|
||||
// Add event listener for theme changes
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener('click', function() {
|
||||
setTimeout(function() {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
document.getElementById('list-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
package list
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata/dialog"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata/utils"
|
||||
)
|
||||
|
||||
// Helper function to generate sorting links
|
||||
func sortLink(currentSortBy, currentSortDir, targetSortBy, basePath string, filter file_metadata.FileMetadataFilter, limit int) string {
|
||||
nextSortDir := "asc"
|
||||
if currentSortBy == targetSortBy && currentSortDir == "asc" {
|
||||
nextSortDir = "desc"
|
||||
}
|
||||
|
||||
q := url.Values{}
|
||||
q.Set("page", "1")
|
||||
q.Set("limit", strconv.Itoa(limit))
|
||||
q.Set("sort_by", targetSortBy)
|
||||
q.Set("sort_dir", nextSortDir)
|
||||
if filter.Status != "" {
|
||||
q.Set("status", filter.Status)
|
||||
}
|
||||
if filter.FileName != "" {
|
||||
q.Set("filename", filter.FileName)
|
||||
}
|
||||
if filter.JobID != "" {
|
||||
q.Set("job_id", filter.JobID)
|
||||
}
|
||||
if filter.Hash != "" {
|
||||
q.Set("hash", filter.Hash)
|
||||
}
|
||||
if filter.StartDate != "" {
|
||||
q.Set("start_date", filter.StartDate)
|
||||
}
|
||||
if filter.EndDate != "" {
|
||||
q.Set("end_date", filter.EndDate)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s?%s", basePath, q.Encode())
|
||||
}
|
||||
|
||||
// Helper function to get sort icon class
|
||||
func sortIconClass(currentSortBy, currentSortDir, targetSortBy string) string {
|
||||
if currentSortBy == targetSortBy {
|
||||
if currentSortDir == "asc" {
|
||||
return "fas fa-sort-up ml-1"
|
||||
}
|
||||
return "fas fa-sort-down ml-1"
|
||||
}
|
||||
return "fas fa-sort text-gray-400 ml-1"
|
||||
}
|
||||
|
||||
// FileMetadataListPartial renders the list of file metadata in a table format
|
||||
// Added basePath and targetContainerID parameters
|
||||
templ FileMetadataListPartial(ctx context.Context, data file_metadata.FileMetadataListData, basePath string, targetContainerID string) {
|
||||
|
||||
<!-- Container for dynamically generated dialogs -->
|
||||
<div id="dialog-container">
|
||||
for _, file := range data.Files {
|
||||
@dialog.FileMetadataDialog(
|
||||
fmt.Sprintf("delete-file-dialog-%d", file.ID),
|
||||
"Delete File Metadata",
|
||||
fmt.Sprintf("Are you sure you want to delete the metadata for '%s'? This cannot be undone.", file.FileName),
|
||||
"text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800",
|
||||
"Delete",
|
||||
"delete",
|
||||
file.ID,
|
||||
file.FileName,
|
||||
"list",
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- File List Table -->
|
||||
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
|
||||
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
|
||||
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
<a
|
||||
href="#"
|
||||
hx-get={ sortLink(data.SortBy, data.SortDir, "id", basePath, data.Filter, data.Limit) }
|
||||
hx-target={ targetContainerID }
|
||||
hx-swap="innerHTML"
|
||||
class="flex items-center hover:text-blue-600 dark:hover:text-blue-400"
|
||||
>
|
||||
ID <i class={ sortIconClass(data.SortBy, data.SortDir, "id") }></i>
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
<a
|
||||
href="#"
|
||||
hx-get={ sortLink(data.SortBy, data.SortDir, "filename", basePath, data.Filter, data.Limit) }
|
||||
hx-target={ targetContainerID }
|
||||
hx-swap="innerHTML"
|
||||
class="flex items-center hover:text-blue-600 dark:hover:text-blue-400"
|
||||
>
|
||||
Filename <i class={ sortIconClass(data.SortBy, data.SortDir, "filename") }></i>
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
<a
|
||||
href="#"
|
||||
hx-get={ sortLink(data.SortBy, data.SortDir, "size", basePath, data.Filter, data.Limit) }
|
||||
hx-target={ targetContainerID }
|
||||
hx-swap="innerHTML"
|
||||
class="flex items-center hover:text-blue-600 dark:hover:text-blue-400"
|
||||
>
|
||||
Size <i class={ sortIconClass(data.SortBy, data.SortDir, "size") }></i>
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
<a
|
||||
href="#"
|
||||
hx-get={ sortLink(data.SortBy, data.SortDir, "processed_time", basePath, data.Filter, data.Limit) }
|
||||
hx-target={ targetContainerID }
|
||||
hx-swap="innerHTML"
|
||||
class="flex items-center hover:text-blue-600 dark:hover:text-blue-400"
|
||||
>
|
||||
Processed time <i class={ sortIconClass(data.SortBy, data.SortDir, "processed_time") }></i>
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
<a
|
||||
href="#"
|
||||
hx-get={ sortLink(data.SortBy, data.SortDir, "status", basePath, data.Filter, data.Limit) }
|
||||
hx-target={ targetContainerID }
|
||||
hx-swap="innerHTML"
|
||||
class="flex items-center hover:text-blue-600 dark:hover:text-blue-400"
|
||||
>
|
||||
Status <i class={ sortIconClass(data.SortBy, data.SortDir, "status") }></i>
|
||||
</a>
|
||||
</th>
|
||||
if data.Job == nil {
|
||||
<th scope="col" class="px-6 py-3">Job</th>
|
||||
}
|
||||
<th scope="col" class="px-6 py-3">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
for _, file := range data.Files {
|
||||
<tr id={ fmt.Sprintf("file-row-%d", file.ID) } class="bg-white border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600">
|
||||
<td class="px-6 py-4 font-medium text-gray-900 dark:text-white">
|
||||
{ strconv.FormatUint(uint64(file.ID), 10) }
|
||||
</td>
|
||||
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
||||
<a href={ templ.SafeURL(fmt.Sprintf("/files/%d", file.ID)) } class="hover:underline">
|
||||
{ file.FileName }
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
{ utils.FormatFileSize(file.FileSize) }
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
{ file.ProcessedTime.Format("2006-01-02 15:04:05") }
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class={ "text-xs font-medium px-2.5 py-0.5 rounded", utils.GetStatusBadgeClass(file.Status) }>
|
||||
{ file.Status }
|
||||
</span>
|
||||
</td>
|
||||
if data.Job == nil {
|
||||
<td class="px-6 py-4">
|
||||
if file.Job.ID > 0 {
|
||||
<a href={ templ.SafeURL(fmt.Sprintf("/files/job/%d", file.Job.ID)) } class="font-medium text-blue-600 dark:text-blue-500 hover:underline">
|
||||
{ file.Job.Name }
|
||||
</a>
|
||||
} else {
|
||||
<span class="text-gray-400 dark:text-gray-500 italic">N/A</span>
|
||||
}
|
||||
</td>
|
||||
}
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex space-x-3">
|
||||
<a href={ templ.SafeURL(fmt.Sprintf("/files/%d", file.ID)) } class="font-medium text-blue-600 dark:text-blue-500 hover:underline">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
onclick={ templ.ComponentScript{Call: fmt.Sprintf("showModal('delete-file-dialog-%d')", file.ID)} }
|
||||
data-file-id={ strconv.FormatUint(uint64(file.ID), 10) }
|
||||
data-file-name={ file.FileName }
|
||||
class="font-medium text-red-600 dark:text-red-500 hover:underline">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination with HTMX (Update links to include sorting and targetContainerID) -->
|
||||
if data.TotalPages > 1 {
|
||||
<nav class="flex items-center flex-column flex-wrap md:flex-row justify-between p-4" aria-label="Table navigation">
|
||||
<span class="text-sm font-normal text-gray-500 dark:text-gray-400 mb-4 md:mb-0">
|
||||
Showing <span class="font-semibold text-gray-900 dark:text-white">{ strconv.Itoa((data.Page-1)*data.Limit+1) }-{ strconv.Itoa(func() int {
|
||||
end := data.Page*data.Limit
|
||||
if int64(end) > data.TotalCount {
|
||||
return int(data.TotalCount)
|
||||
}
|
||||
return end
|
||||
}()) }</span> of <span class="font-semibold text-gray-900 dark:text-white">{ strconv.FormatInt(data.TotalCount, 10) }</span>
|
||||
</span>
|
||||
<ul class="inline-flex -space-x-px rtl:space-x-reverse text-sm h-8">
|
||||
<li>
|
||||
if data.Page == 1 {
|
||||
<span class="flex items-center justify-center px-3 h-8 ms-0 leading-tight text-gray-500 bg-white border border-gray-300 rounded-s-lg cursor-not-allowed dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400">
|
||||
Previous
|
||||
</span>
|
||||
} else {
|
||||
<a hx-get={ fmt.Sprintf("%s?page=%d&limit=%d&status=%s&filename=%s&job_id=%s&sort_by=%s&sort_dir=%s", basePath, data.Page - 1, data.Limit, data.Filter.Status, data.Filter.FileName, data.Filter.JobID, data.SortBy, data.SortDir) }
|
||||
hx-target={ targetContainerID }
|
||||
hx-swap="innerHTML"
|
||||
class="flex items-center justify-center px-3 h-8 ms-0 leading-tight text-gray-500 bg-white border border-gray-300 rounded-s-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">
|
||||
Previous
|
||||
</a>
|
||||
}
|
||||
</li>
|
||||
|
||||
for i := 1; i <= data.TotalPages; i++ {
|
||||
if i == 1 || i == data.TotalPages || (i >= data.Page-2 && i <= data.Page+2) {
|
||||
<li>
|
||||
if i == data.Page {
|
||||
<span aria-current="page" class="flex items-center justify-center px-3 h-8 text-blue-600 border border-gray-300 bg-blue-50 hover:bg-blue-100 hover:text-blue-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white">
|
||||
{ strconv.Itoa(i) }
|
||||
</span>
|
||||
} else {
|
||||
<a hx-get={ fmt.Sprintf("%s?page=%d&limit=%d&status=%s&filename=%s&job_id=%s&sort_by=%s&sort_dir=%s", basePath, i, data.Limit, data.Filter.Status, data.Filter.FileName, data.Filter.JobID, data.SortBy, data.SortDir) }
|
||||
hx-target={ targetContainerID }
|
||||
hx-swap="innerHTML"
|
||||
class="flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">
|
||||
{ strconv.Itoa(i) }
|
||||
</a>
|
||||
}
|
||||
</li>
|
||||
} else if (i == 2 && data.Page > 4) || (i == data.TotalPages-1 && data.Page < data.TotalPages-3) {
|
||||
<li>
|
||||
<span class="flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400">
|
||||
...
|
||||
</span>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
<li>
|
||||
if data.Page == data.TotalPages {
|
||||
<span class="flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 rounded-e-lg cursor-not-allowed dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400">
|
||||
Next
|
||||
</span>
|
||||
} else {
|
||||
<a hx-get={ fmt.Sprintf("%s?page=%d&limit=%d&status=%s&filename=%s&job_id=%s&sort_by=%s&sort_dir=%s", basePath, data.Page + 1, data.Limit, data.Filter.Status, data.Filter.FileName, data.Filter.JobID, data.SortBy, data.SortDir) }
|
||||
hx-target={ targetContainerID }
|
||||
hx-swap="innerHTML"
|
||||
class="flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 rounded-e-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">
|
||||
Next
|
||||
</a>
|
||||
}
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,802 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package list
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata/dialog"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata/utils"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Helper function to generate sorting links
|
||||
func sortLink(currentSortBy, currentSortDir, targetSortBy, basePath string, filter file_metadata.FileMetadataFilter, limit int) string {
|
||||
nextSortDir := "asc"
|
||||
if currentSortBy == targetSortBy && currentSortDir == "asc" {
|
||||
nextSortDir = "desc"
|
||||
}
|
||||
|
||||
q := url.Values{}
|
||||
q.Set("page", "1")
|
||||
q.Set("limit", strconv.Itoa(limit))
|
||||
q.Set("sort_by", targetSortBy)
|
||||
q.Set("sort_dir", nextSortDir)
|
||||
if filter.Status != "" {
|
||||
q.Set("status", filter.Status)
|
||||
}
|
||||
if filter.FileName != "" {
|
||||
q.Set("filename", filter.FileName)
|
||||
}
|
||||
if filter.JobID != "" {
|
||||
q.Set("job_id", filter.JobID)
|
||||
}
|
||||
if filter.Hash != "" {
|
||||
q.Set("hash", filter.Hash)
|
||||
}
|
||||
if filter.StartDate != "" {
|
||||
q.Set("start_date", filter.StartDate)
|
||||
}
|
||||
if filter.EndDate != "" {
|
||||
q.Set("end_date", filter.EndDate)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s?%s", basePath, q.Encode())
|
||||
}
|
||||
|
||||
// Helper function to get sort icon class
|
||||
func sortIconClass(currentSortBy, currentSortDir, targetSortBy string) string {
|
||||
if currentSortBy == targetSortBy {
|
||||
if currentSortDir == "asc" {
|
||||
return "fas fa-sort-up ml-1"
|
||||
}
|
||||
return "fas fa-sort-down ml-1"
|
||||
}
|
||||
return "fas fa-sort text-gray-400 ml-1"
|
||||
}
|
||||
|
||||
// FileMetadataListPartial renders the list of file metadata in a table format
|
||||
// Added basePath and targetContainerID parameters
|
||||
func FileMetadataListPartial(ctx context.Context, data file_metadata.FileMetadataListData, basePath string, targetContainerID string) 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!-- Container for dynamically generated dialogs --><div id=\"dialog-container\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, file := range data.Files {
|
||||
templ_7745c5c3_Err = dialog.FileMetadataDialog(
|
||||
fmt.Sprintf("delete-file-dialog-%d", file.ID),
|
||||
"Delete File Metadata",
|
||||
fmt.Sprintf("Are you sure you want to delete the metadata for '%s'? This cannot be undone.", file.FileName),
|
||||
"text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800",
|
||||
"Delete",
|
||||
"delete",
|
||||
file.ID,
|
||||
file.FileName,
|
||||
"list",
|
||||
).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div><!-- File List Table --><div class=\"relative overflow-x-auto shadow-md sm:rounded-lg\"><table class=\"w-full text-sm text-left text-gray-500 dark:text-gray-400\"><thead class=\"text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400\"><tr><th scope=\"col\" class=\"px-6 py-3\"><a href=\"#\" hx-get=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(sortLink(data.SortBy, data.SortDir, "id", basePath, data.Filter, data.Limit))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 87, Col: 92}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" hx-target=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(targetContainerID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 88, Col: 36}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" hx-swap=\"innerHTML\" class=\"flex items-center hover:text-blue-600 dark:hover:text-blue-400\">ID ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 = []any{sortIconClass(data.SortBy, data.SortDir, "id")}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var4...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<i class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var4).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\"></i></a></th><th scope=\"col\" class=\"px-6 py-3\"><a href=\"#\" hx-get=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(sortLink(data.SortBy, data.SortDir, "filename", basePath, data.Filter, data.Limit))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 98, Col: 98}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" hx-target=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(targetContainerID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 99, Col: 36}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" hx-swap=\"innerHTML\" class=\"flex items-center hover:text-blue-600 dark:hover:text-blue-400\">Filename ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 = []any{sortIconClass(data.SortBy, data.SortDir, "filename")}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<i class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var8).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\"></i></a></th><th scope=\"col\" class=\"px-6 py-3\"><a href=\"#\" hx-get=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(sortLink(data.SortBy, data.SortDir, "size", basePath, data.Filter, data.Limit))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 109, Col: 94}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" hx-target=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(targetContainerID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 110, Col: 36}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" hx-swap=\"innerHTML\" class=\"flex items-center hover:text-blue-600 dark:hover:text-blue-400\">Size ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 = []any{sortIconClass(data.SortBy, data.SortDir, "size")}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<i class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var12).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\"></i></a></th><th scope=\"col\" class=\"px-6 py-3\"><a href=\"#\" hx-get=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(sortLink(data.SortBy, data.SortDir, "processed_time", basePath, data.Filter, data.Limit))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 120, Col: 104}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" hx-target=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(targetContainerID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 121, Col: 36}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" hx-swap=\"innerHTML\" class=\"flex items-center hover:text-blue-600 dark:hover:text-blue-400\">Processed time ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var16 = []any{sortIconClass(data.SortBy, data.SortDir, "processed_time")}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var16...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<i class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var16).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\"></i></a></th><th scope=\"col\" class=\"px-6 py-3\"><a href=\"#\" hx-get=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(sortLink(data.SortBy, data.SortDir, "status", basePath, data.Filter, data.Limit))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 131, Col: 96}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" hx-target=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(targetContainerID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 132, Col: 36}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" hx-swap=\"innerHTML\" class=\"flex items-center hover:text-blue-600 dark:hover:text-blue-400\">Status ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var20 = []any{sortIconClass(data.SortBy, data.SortDir, "status")}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var20...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<i class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var20).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\"></i></a></th>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Job == nil {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<th scope=\"col\" class=\"px-6 py-3\">Job</th>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<th scope=\"col\" class=\"px-6 py-3\">Actions</th></tr></thead> <tbody>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, file := range data.Files {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<tr id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var22 string
|
||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("file-row-%d", file.ID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 147, Col: 49}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" class=\"bg-white border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600\"><td class=\"px-6 py-4 font-medium text-gray-900 dark:text-white\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var23 string
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.FormatUint(uint64(file.ID), 10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 149, Col: 48}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</td><td class=\"px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var24 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/files/%d", file.ID))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var24)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" class=\"hover:underline\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var25 string
|
||||
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(file.FileName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 153, Col: 23}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</a></td><td class=\"px-6 py-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var26 string
|
||||
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(utils.FormatFileSize(file.FileSize))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 157, Col: 44}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</td><td class=\"px-6 py-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var27 string
|
||||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(file.ProcessedTime.Format("2006-01-02 15:04:05"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 160, Col: 57}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</td><td class=\"px-6 py-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var28 = []any{"text-xs font-medium px-2.5 py-0.5 rounded", utils.GetStatusBadgeClass(file.Status)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var28...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<span class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var29 string
|
||||
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var28).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var30 string
|
||||
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(file.Status)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 164, Col: 21}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</span></td>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Job == nil {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<td class=\"px-6 py-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if file.Job.ID > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var31 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/files/job/%d", file.Job.ID))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var31)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "\" class=\"font-medium text-blue-600 dark:text-blue-500 hover:underline\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var32 string
|
||||
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(file.Job.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 171, Col: 25}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<span class=\"text-gray-400 dark:text-gray-500 italic\">N/A</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</td>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<td class=\"px-6 py-4\"><div class=\"flex space-x-3\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var33 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/files/%d", file.ID))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var33)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "\" class=\"font-medium text-blue-600 dark:text-blue-500 hover:underline\"><i class=\"fas fa-eye\"></i></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("showModal('delete-file-dialog-%d')", file.ID)})
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<button type=\"button\" onclick=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var34 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("showModal('delete-file-dialog-%d')", file.ID)}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34.Call)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\" data-file-id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var35 string
|
||||
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.FormatUint(uint64(file.ID), 10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 186, Col: 63}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\" data-file-name=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var36 string
|
||||
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(file.FileName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 187, Col: 39}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\" class=\"font-medium text-red-600 dark:text-red-500 hover:underline\"><i class=\"fas fa-trash\"></i></button></div></td></tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</tbody></table></div><!-- Pagination with HTMX (Update links to include sorting and targetContainerID) -->")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.TotalPages > 1 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "<nav class=\"flex items-center flex-column flex-wrap md:flex-row justify-between p-4\" aria-label=\"Table navigation\"><span class=\"text-sm font-normal text-gray-500 dark:text-gray-400 mb-4 md:mb-0\">Showing <span class=\"font-semibold text-gray-900 dark:text-white\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var37 string
|
||||
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa((data.Page-1)*data.Limit + 1))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 203, Col: 112}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "-")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var38 string
|
||||
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(func() int {
|
||||
end := data.Page * data.Limit
|
||||
if int64(end) > data.TotalCount {
|
||||
return int(data.TotalCount)
|
||||
}
|
||||
return end
|
||||
}()))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 209, Col: 8}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</span> of <span class=\"font-semibold text-gray-900 dark:text-white\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var39 string
|
||||
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.FormatInt(data.TotalCount, 10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 209, Col: 119}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</span></span><ul class=\"inline-flex -space-x-px rtl:space-x-reverse text-sm h-8\"><li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Page == 1 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<span class=\"flex items-center justify-center px-3 h-8 ms-0 leading-tight text-gray-500 bg-white border border-gray-300 rounded-s-lg cursor-not-allowed dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400\">Previous</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "<a hx-get=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var40 string
|
||||
templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s?page=%d&limit=%d&status=%s&filename=%s&job_id=%s&sort_by=%s&sort_dir=%s", basePath, data.Page-1, data.Limit, data.Filter.Status, data.Filter.FileName, data.Filter.JobID, data.SortBy, data.SortDir))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 218, Col: 232}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\" hx-target=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var41 string
|
||||
templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(targetContainerID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 219, Col: 38}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\" hx-swap=\"innerHTML\" class=\"flex items-center justify-center px-3 h-8 ms-0 leading-tight text-gray-500 bg-white border border-gray-300 rounded-s-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white\">Previous</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for i := 1; i <= data.TotalPages; i++ {
|
||||
if i == 1 || i == data.TotalPages || (i >= data.Page-2 && i <= data.Page+2) {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if i == data.Page {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<span aria-current=\"page\" class=\"flex items-center justify-center px-3 h-8 text-blue-600 border border-gray-300 bg-blue-50 hover:bg-blue-100 hover:text-blue-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var42 string
|
||||
templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(i))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 232, Col: 26}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<a hx-get=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var43 string
|
||||
templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s?page=%d&limit=%d&status=%s&filename=%s&job_id=%s&sort_by=%s&sort_dir=%s", basePath, i, data.Limit, data.Filter.Status, data.Filter.FileName, data.Filter.JobID, data.SortBy, data.SortDir))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 235, Col: 222}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "\" hx-target=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var44 string
|
||||
templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(targetContainerID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 236, Col: 40}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\" hx-swap=\"innerHTML\" class=\"flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var45 string
|
||||
templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(i))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 239, Col: 26}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else if (i == 2 && data.Page > 4) || (i == data.TotalPages-1 && data.Page < data.TotalPages-3) {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "<li><span class=\"flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400\">...</span></li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Page == data.TotalPages {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "<span class=\"flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 rounded-e-lg cursor-not-allowed dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400\">Next</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<a hx-get=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var46 string
|
||||
templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s?page=%d&limit=%d&status=%s&filename=%s&job_id=%s&sort_by=%s&sort_dir=%s", basePath, data.Page+1, data.Limit, data.Filter.Status, data.Filter.FileName, data.Filter.JobID, data.SortBy, data.SortDir))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 258, Col: 232}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "\" hx-target=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var47 string
|
||||
templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(targetContainerID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 259, Col: 38}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "\" hx-swap=\"innerHTML\" class=\"flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 rounded-e-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white\">Next</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</li></ul></nav>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,208 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package list
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/starfleetcptn/gomft/components" // Import the main components package
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata/utils"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// FileMetadataList renders the list of file metadata
|
||||
func FileMetadataList(ctx context.Context, data file_metadata.FileMetadataListData) 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, " <!-- Status and Error Messages --> <div id=\"toast-container\" class=\"fixed top-5 right-5 z-50 flex flex-col gap-2\"></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = utils.FileMetadataJS().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " <div id=\"list-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\"><h1 class=\"text-2xl font-bold text-gray-900 dark:text-white flex items-center\"><i class=\"fas fa-file-alt w-6 h-6 mr-2 text-blue-500\"></i> Files</h1><div class=\"flex gap-3\"><a href=\"/files/search\" class=\"text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-search mr-2\"></i> Advanced Search</a></div></div><!-- Filter Form --><div class=\"p-4 mb-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full\"><h5 class=\"mb-4 text-lg font-semibold text-gray-900 dark:text-white\">Filter Files</h5><form hx-get=\"/files/partial\" hx-target=\"#file-list-container\" hx-swap=\"innerHTML\" hx-indicator=\"#filter-loading\" hx-headers=\"{"X-HX-Request": "true"}\" class=\"grid grid-cols-1 md:grid-cols-3 gap-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Job == nil {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div><label for=\"job_id\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Job</label> <input type=\"text\" id=\"job_id\" name=\"job_id\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.Filter.JobID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list.templ`, Line: 46, Col: 78}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" placeholder=\"Job ID\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div><label for=\"status\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Status</label> <select id=\"status\" name=\"status\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"><option value=\"\">All Statuses</option> <option value=\"processed\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Filter.Status == "processed" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, ">Processed</option> <option value=\"archived\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Filter.Status == "archived" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, ">Archived</option> <option value=\"deleted\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Filter.Status == "deleted" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, ">Deleted</option> <option value=\"archived_and_deleted\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Filter.Status == "archived_and_deleted" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, ">Archived & Deleted</option> <option value=\"error\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Filter.Status == "error" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, ">Error</option></select></div><div><label for=\"filename\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Filename</label> <input type=\"text\" id=\"filename\" name=\"filename\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Filter.FileName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list.templ`, Line: 62, Col: 84}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" placeholder=\"Filename or partial match\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"></div><div class=\"md:col-span-3 flex justify-end items-center\"><button type=\"submit\" class=\"text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-filter mr-2\"></i> Apply Filters</button><div id=\"filter-loading\" class=\"htmx-indicator ml-2 flex items-center\"><i class=\"fas fa-circle-notch fa-spin text-blue-600\"></i></div></div></form></div><div class=\"bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full\"><!-- Card header --><div class=\"p-4 md:p-5 border-b border-gray-200 dark:border-gray-700\"><h5 class=\"text-xl font-bold leading-none text-gray-900 dark:text-white\">File List</h5><p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Showing ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.FormatInt(int64((data.Page-1)*data.Limit+1), 10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list.templ`, Line: 84, Col: 79}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, " to ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.FormatInt(int64(min(data.Page*data.Limit, int(data.TotalCount))), 10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list.templ`, Line: 84, Col: 166}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " of ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.FormatInt(data.TotalCount, 10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list.templ`, Line: 84, Col: 212}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, " files</p></div><!-- Card content --><div id=\"file-list-container\" class=\"p-4 md:p-5\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = FileMetadataListPartial(ctx, data, "/files/partial", "#file-list-container").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div></div></div><script>\n\t\t\t\t// Set dark background color if in dark mode\n\t\t\t\tif (document.documentElement.classList.contains('dark')) {\n\t\t\t\t\tdocument.getElementById('list-container').style.backgroundColor = '#111827';\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Add event listener for theme changes\n\t\t\t\tdocument.addEventListener('DOMContentLoaded', function() {\n\t\t\t\t\tconst themeToggle = document.getElementById('theme-toggle');\n\t\t\t\t\tif (themeToggle) {\n\t\t\t\t\t\tthemeToggle.addEventListener('click', function() {\n\t\t\t\t\t\t\tsetTimeout(function() {\n\t\t\t\t\t\t\t\tconst isDark = document.documentElement.classList.contains('dark');\n\t\t\t\t\t\t\t\tdocument.getElementById('list-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';\n\t\t\t\t\t\t\t}, 50);\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t</script></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = components.LayoutWithContext("Files", ctx).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,131 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata/utils"
|
||||
"github.com/starfleetcptn/gomft/components" // Import the main components package
|
||||
)
|
||||
|
||||
// FileMetadataSearch renders the search interface for file metadata
|
||||
templ FileMetadataSearch(ctx context.Context, data file_metadata.FileMetadataSearchData) {
|
||||
@components.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>
|
||||
|
||||
@utils.FileMetadataJS() // Include JS for toasts, etc.
|
||||
|
||||
<div id="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">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||
<i class="fas fa-search w-6 h-6 mr-2 text-blue-500"></i>
|
||||
Search Files
|
||||
</h1>
|
||||
<div class="flex gap-3">
|
||||
<a href="/files" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||
<i class="fas fa-arrow-left mr-2"></i> Back to Files
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced Search Form -->
|
||||
<div class="p-4 mb-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full">
|
||||
<h5 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">Advanced File Search</h5>
|
||||
<form
|
||||
hx-get="/files/search/partial"
|
||||
hx-target="#search-results-container"
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#search-form-loading"
|
||||
hx-headers='{"X-HX-Request": "true"}'
|
||||
hx-boost="false"
|
||||
class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="job_id" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Job ID</label>
|
||||
<input type="text" id="job_id" name="job_id" value={ data.Filter.JobID } placeholder="Job ID"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="status" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Status</label>
|
||||
<select id="status" name="status" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="processed" selected?={ data.Filter.Status == "processed" }>Processed</option>
|
||||
<option value="archived" selected?={ data.Filter.Status == "archived" }>Archived</option>
|
||||
<option value="deleted" selected?={ data.Filter.Status == "deleted" }>Deleted</option>
|
||||
<option value="archived_and_deleted" selected?={ data.Filter.Status == "archived_and_deleted" }>Archived & Deleted</option>
|
||||
<option value="error" selected?={ data.Filter.Status == "error" }>Error</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="filename" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Filename</label>
|
||||
<input type="text" id="filename" name="filename" value={ data.Filter.FileName } placeholder="Filename or partial match"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="hash" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">File Hash</label>
|
||||
<input type="text" id="hash" name="hash" value={ data.Filter.Hash } placeholder="MD5 hash"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="start_date" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Processed After</label>
|
||||
<input type="date" id="start_date" name="start_date" value={ data.Filter.StartDate }
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="end_date" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Processed Before</label>
|
||||
<input type="date" id="end_date" name="end_date" value={ data.Filter.EndDate }
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||
</div>
|
||||
<div class="md:col-span-2 flex justify-end">
|
||||
<button type="submit" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||
<i class="fas fa-search mr-2"></i> Search Files
|
||||
</button>
|
||||
<div id="search-form-loading" class="htmx-indicator ml-2 flex items-center">
|
||||
<i class="fas fa-circle-notch fa-spin text-blue-600"></i>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Results Container (Initially empty, populated by HTMX) -->
|
||||
<div id="search-results-container" class="bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full mt-6">
|
||||
<div class="p-4 md:p-5 border-b border-gray-200 dark:border-gray-700">
|
||||
<h5 class="text-xl font-bold leading-none text-gray-900 dark:text-white">
|
||||
Search Results
|
||||
</h5>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Enter search criteria above and click "Search Files".
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-4 md:p-5">
|
||||
<!-- Content will be loaded here by HTMX -->
|
||||
<div class="text-center text-gray-500 dark:text-gray-400 py-8">
|
||||
No results yet.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Set dark background color if in dark mode
|
||||
if (document.documentElement.classList.contains('dark')) {
|
||||
document.getElementById('search-container').style.backgroundColor = '#111827';
|
||||
}
|
||||
|
||||
// Add event listener for theme changes
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener('click', function() {
|
||||
setTimeout(function() {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
document.getElementById('search-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata/list"
|
||||
)
|
||||
|
||||
// FileMetadataSearchContent renders only the search results table and pagination
|
||||
templ FileMetadataSearchContent(ctx context.Context, data file_metadata.FileMetadataSearchData) {
|
||||
<!-- Search Results -->
|
||||
<div id="search-results">
|
||||
if len(data.Files) > 0 {
|
||||
@list.FileMetadataListPartial(ctx, file_metadata.FileMetadataListData{
|
||||
Files: data.Files,
|
||||
Page: data.Page,
|
||||
Limit: data.Limit,
|
||||
TotalCount: data.TotalCount,
|
||||
TotalPages: data.TotalPages,
|
||||
Filter: data.Filter, // Pass filter data for pagination links
|
||||
SortBy: data.SortBy,
|
||||
SortDir: data.SortDir,
|
||||
}, "/files/search/partial", "#search-results-container") // Pass correct base path and target ID
|
||||
} else {
|
||||
<div class="p-6 text-center text-gray-500 dark:text-gray-400">
|
||||
<svg class="mx-auto mb-4 w-12 h-12 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
||||
</svg>
|
||||
<p>No files found matching your search criteria.</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package search
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata/list"
|
||||
)
|
||||
|
||||
// FileMetadataSearchContent renders only the search results table and pagination
|
||||
func FileMetadataSearchContent(ctx context.Context, data file_metadata.FileMetadataSearchData) 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!-- Search Results --><div id=\"search-results\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(data.Files) > 0 {
|
||||
templ_7745c5c3_Err = list.FileMetadataListPartial(ctx, file_metadata.FileMetadataListData{
|
||||
Files: data.Files,
|
||||
Page: data.Page,
|
||||
Limit: data.Limit,
|
||||
TotalCount: data.TotalCount,
|
||||
TotalPages: data.TotalPages,
|
||||
Filter: data.Filter, // Pass filter data for pagination links
|
||||
SortBy: data.SortBy,
|
||||
SortDir: data.SortDir,
|
||||
}, "/files/search/partial", "#search-results-container").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"p-6 text-center text-gray-500 dark:text-gray-400\"><svg class=\"mx-auto mb-4 w-12 h-12 text-gray-400\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z\"></path></svg><p>No files found matching your search criteria.</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,189 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package search
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/starfleetcptn/gomft/components" // Import the main components package
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata/utils"
|
||||
)
|
||||
|
||||
// FileMetadataSearch renders the search interface for file metadata
|
||||
func FileMetadataSearch(ctx context.Context, data file_metadata.FileMetadataSearchData) 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!-- Status and Error Messages --> <div id=\"toast-container\" class=\"fixed top-5 right-5 z-50 flex flex-col gap-2\"></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = utils.FileMetadataJS().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " <div id=\"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\"><h1 class=\"text-2xl font-bold text-gray-900 dark:text-white flex items-center\"><i class=\"fas fa-search w-6 h-6 mr-2 text-blue-500\"></i> Search Files</h1><div class=\"flex gap-3\"><a href=\"/files\" class=\"text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-arrow-left mr-2\"></i> Back to Files</a></div></div><!-- Advanced Search Form --><div class=\"p-4 mb-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full\"><h5 class=\"mb-4 text-lg font-semibold text-gray-900 dark:text-white\">Advanced File Search</h5><form hx-get=\"/files/search/partial\" hx-target=\"#search-results-container\" hx-swap=\"innerHTML\" hx-indicator=\"#search-form-loading\" hx-headers=\"{"X-HX-Request": "true"}\" hx-boost=\"false\" class=\"grid grid-cols-1 md:grid-cols-2 gap-4\"><div><label for=\"job_id\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Job ID</label> <input type=\"text\" id=\"job_id\" name=\"job_id\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.Filter.JobID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/search/file_metadata_search.templ`, Line: 45, Col: 77}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" placeholder=\"Job ID\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"></div><div><label for=\"status\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Status</label> <select id=\"status\" name=\"status\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"><option value=\"\">All Statuses</option> <option value=\"processed\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Filter.Status == "processed" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, ">Processed</option> <option value=\"archived\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Filter.Status == "archived" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, ">Archived</option> <option value=\"deleted\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Filter.Status == "deleted" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, ">Deleted</option> <option value=\"archived_and_deleted\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Filter.Status == "archived_and_deleted" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, ">Archived & Deleted</option> <option value=\"error\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Filter.Status == "error" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, ">Error</option></select></div><div><label for=\"filename\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Filename</label> <input type=\"text\" id=\"filename\" name=\"filename\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Filter.FileName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/search/file_metadata_search.templ`, Line: 61, Col: 84}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" placeholder=\"Filename or partial match\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"></div><div><label for=\"hash\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">File Hash</label> <input type=\"text\" id=\"hash\" name=\"hash\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.Filter.Hash)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/search/file_metadata_search.templ`, Line: 66, Col: 72}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" placeholder=\"MD5 hash\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"></div><div><label for=\"start_date\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Processed After</label> <input type=\"date\" id=\"start_date\" name=\"start_date\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(data.Filter.StartDate)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/search/file_metadata_search.templ`, Line: 71, Col: 89}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"></div><div><label for=\"end_date\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Processed Before</label> <input type=\"date\" id=\"end_date\" name=\"end_date\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(data.Filter.EndDate)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/search/file_metadata_search.templ`, Line: 76, Col: 83}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"></div><div class=\"md:col-span-2 flex justify-end\"><button type=\"submit\" class=\"text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-search mr-2\"></i> Search Files</button><div id=\"search-form-loading\" class=\"htmx-indicator ml-2 flex items-center\"><i class=\"fas fa-circle-notch fa-spin text-blue-600\"></i></div></div></form></div><!-- Results Container (Initially empty, populated by HTMX) --><div id=\"search-results-container\" class=\"bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full mt-6\"><div class=\"p-4 md:p-5 border-b border-gray-200 dark:border-gray-700\"><h5 class=\"text-xl font-bold leading-none text-gray-900 dark:text-white\">Search Results</h5><p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Enter search criteria above and click \"Search Files\".</p></div><div class=\"p-4 md:p-5\"><!-- Content will be loaded here by HTMX --><div class=\"text-center text-gray-500 dark:text-gray-400 py-8\">No results yet.</div></div></div></div><script>\n\t\t\t\t// Set dark background color if in dark mode\n\t\t\t\tif (document.documentElement.classList.contains('dark')) {\n\t\t\t\t\tdocument.getElementById('search-container').style.backgroundColor = '#111827';\n\t\t\t\t}\n\n\t\t\t\t// Add event listener for theme changes\n\t\t\t\tdocument.addEventListener('DOMContentLoaded', function() {\n\t\t\t\t\tconst themeToggle = document.getElementById('theme-toggle');\n\t\t\t\t\tif (themeToggle) {\n\t\t\t\t\t\tthemeToggle.addEventListener('click', function() {\n\t\t\t\t\t\t\tsetTimeout(function() {\n\t\t\t\t\t\t\t\tconst isDark = document.documentElement.classList.contains('dark');\n\t\t\t\t\t\t\t\tdocument.getElementById('search-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';\n\t\t\t\t\t\t\t}, 50);\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t</script></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = components.LayoutWithContext("Search Files", ctx).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,43 @@
|
||||
package file_metadata
|
||||
|
||||
import "github.com/starfleetcptn/gomft/internal/db"
|
||||
|
||||
// FileMetadataFilter represents filter parameters for file metadata queries
|
||||
type FileMetadataFilter struct {
|
||||
Status string
|
||||
JobID string
|
||||
FileName string
|
||||
Hash string
|
||||
StartDate string
|
||||
EndDate string
|
||||
}
|
||||
|
||||
// FileMetadataListData contains data for the file metadata list template
|
||||
type FileMetadataListData struct {
|
||||
Files []db.FileMetadata
|
||||
TotalCount int64
|
||||
Page int
|
||||
Limit int
|
||||
TotalPages int
|
||||
Job *db.Job // Optional: if viewing files for a specific job
|
||||
Filter FileMetadataFilter
|
||||
SortBy string // Added for sorting
|
||||
SortDir string // Added for sorting ("asc" or "desc")
|
||||
}
|
||||
|
||||
// FileMetadataDetailsData contains data for the file metadata details template
|
||||
type FileMetadataDetailsData struct {
|
||||
File db.FileMetadata
|
||||
}
|
||||
|
||||
// FileMetadataSearchData contains data for the file metadata search template
|
||||
type FileMetadataSearchData struct {
|
||||
Files []db.FileMetadata
|
||||
TotalCount int64
|
||||
Page int
|
||||
Limit int
|
||||
TotalPages int
|
||||
Filter FileMetadataFilter
|
||||
SortBy string // Added for sorting
|
||||
SortDir string // Added for sorting ("asc" or "desc")
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package utils
|
||||
|
||||
// FileMetadataJS provides common JavaScript functions for file metadata components,
|
||||
// including HTMX event listeners for delete toasts.
|
||||
templ FileMetadataJS() {
|
||||
<script>
|
||||
// Function to create and show a toast (Defined locally for guaranteed availability)
|
||||
function showToast(message, type = 'info') {
|
||||
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.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';
|
||||
|
||||
// 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';
|
||||
}
|
||||
|
||||
// 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.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);
|
||||
}
|
||||
|
||||
// --- HTMX Event Listener for Delete Toasts ---
|
||||
|
||||
// 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;
|
||||
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Set the flag to true after attaching the listener
|
||||
window._gomft_fileMetadataListenerAttached = true;
|
||||
console.log("[FileMetadataJS] HTMX afterRequest listener attached."); // Log attachment once
|
||||
}
|
||||
|
||||
</script>
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,34 @@
|
||||
package utils
|
||||
|
||||
import "fmt"
|
||||
|
||||
// GetStatusBadgeClass returns the appropriate CSS class for a file status badge
|
||||
func GetStatusBadgeClass(status string) string {
|
||||
switch status {
|
||||
case "processed":
|
||||
return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300"
|
||||
case "archived":
|
||||
return "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300"
|
||||
case "deleted":
|
||||
return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300"
|
||||
case "archived_and_deleted":
|
||||
return "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300"
|
||||
case "error":
|
||||
return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300"
|
||||
default:
|
||||
return "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300"
|
||||
}
|
||||
}
|
||||
|
||||
// FormatFileSize formats a file size in bytes to a human-readable string
|
||||
func FormatFileSize(size int64) string {
|
||||
if size < 1024 {
|
||||
return fmt.Sprintf("%d B", size)
|
||||
} else if size < 1024*1024 {
|
||||
return fmt.Sprintf("%.2f KB", float64(size)/1024)
|
||||
} else if size < 1024*1024*1024 {
|
||||
return fmt.Sprintf("%.2f MB", float64(size)/(1024*1024))
|
||||
} else {
|
||||
return fmt.Sprintf("%.2f GB", float64(size)/(1024*1024*1024))
|
||||
}
|
||||
}
|
||||
+8
-19
@@ -8,7 +8,10 @@ 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 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 id={ id } tabindex="-1" aria-hidden="true" class="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">
|
||||
<!-- Backdrop -->
|
||||
<div id={ fmt.Sprintf("%s-backdrop", id) } class="fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm"></div>
|
||||
<!-- Modal content -->
|
||||
<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">
|
||||
@@ -30,7 +33,7 @@ templ JobDialog(id string, title string, message string, confirmClass string, co
|
||||
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">
|
||||
<button type="button" onclick={ closeModal(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>
|
||||
@@ -39,16 +42,6 @@ templ JobDialog(id string, title string, message string, confirmClass string, co
|
||||
</div>
|
||||
}
|
||||
|
||||
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");
|
||||
@@ -196,9 +189,7 @@ templ Jobs(ctx context.Context, data JobsData) {
|
||||
});
|
||||
|
||||
// Track all HTMX events for debugging
|
||||
document.addEventListener('htmx:beforeRequest', function(event) {
|
||||
console.log("HTMX before request:", event.detail);
|
||||
|
||||
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;
|
||||
@@ -215,9 +206,7 @@ templ Jobs(ctx context.Context, data JobsData) {
|
||||
});
|
||||
|
||||
// Track HTMX after-request events for job deletion
|
||||
document.addEventListener('htmx:afterRequest', function(event) {
|
||||
console.log("HTMX after request:", event.detail);
|
||||
|
||||
document.addEventListener('htmx:afterRequest', function(event) {
|
||||
// Check for job deletion multiple ways
|
||||
const isDeleteRequest =
|
||||
// Check global flag from the triggerJobDelete function
|
||||
@@ -395,7 +384,7 @@ templ Jobs(ctx context.Context, data JobsData) {
|
||||
)
|
||||
<button
|
||||
type="button"
|
||||
onclick={ showJobDialog(fmt.Sprintf("delete-job-dialog-%d", job.ID)) }
|
||||
onclick={ showModal(fmt.Sprintf("delete-job-dialog-%d", job.ID)) }
|
||||
class="focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-xs px-3 py-1.5 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-900">
|
||||
<i class="fas fa-trash mr-1"></i>
|
||||
Delete
|
||||
|
||||
+122
-97
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
"github.com/gin-gonic/gin"
|
||||
"time"
|
||||
"github.com/starfleetcptn/gomft/components/shared/toast"
|
||||
)
|
||||
|
||||
// AppVersion will be set at build time using ldflags
|
||||
@@ -54,42 +55,36 @@ templ LayoutWithContext(title string, ctx context.Context) {
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png"/>
|
||||
<link rel="manifest" href="/static/site.webmanifest"/>
|
||||
<title>{ title } - GoMFT</title>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
<script src="https://unpkg.com/alpinejs@3.13.5/dist/cdn.min.js" defer></script>
|
||||
<!-- Tailwind CSS with Flowbite -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.2.1/flowbite.min.css" rel="stylesheet"/>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.2.1/flowbite.min.js"></script>
|
||||
<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"/>
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="/static/dist/fontawesome/css/all.min.css"/>
|
||||
<!-- Application assets -->
|
||||
<link rel="stylesheet" href="/static/dist/app.css"/>
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css"/>
|
||||
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#f0f9ff',
|
||||
100: '#e0f2fe',
|
||||
200: '#bae6fd',
|
||||
300: '#7dd3fc',
|
||||
400: '#38bdf8',
|
||||
500: '#0ea5e9',
|
||||
600: '#0284c7',
|
||||
700: '#0369a1',
|
||||
800: '#075985',
|
||||
900: '#0c4a6e',
|
||||
950: '#082f49',
|
||||
// Immediate theme application
|
||||
// This script runs before the DOM is fully loaded
|
||||
(function() {
|
||||
const isDark = localStorage.theme === 'dark' ||
|
||||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
|
||||
if (isDark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.body.classList.add('dark');
|
||||
|
||||
// Apply dark theme to containers
|
||||
const containers = ['jobs-container', 'configs-container'];
|
||||
containers.forEach(function(id) {
|
||||
const container = document.getElementById(id);
|
||||
if (container) {
|
||||
container.classList.add('dark');
|
||||
container.style.backgroundColor = '#111827';
|
||||
}
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter var', 'ui-sans-serif', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Helvetica Neue', 'Arial', 'sans-serif'],
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<style>
|
||||
html, body {
|
||||
@@ -146,34 +141,8 @@ templ LayoutWithContext(title string, ctx context.Context) {
|
||||
background-color: #1f2937 !important;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css"/>
|
||||
<script>
|
||||
// Immediate theme application
|
||||
// This script runs before the DOM is fully loaded
|
||||
(function() {
|
||||
const isDark = localStorage.theme === 'dark' ||
|
||||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
|
||||
if (isDark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.body.classList.add('dark');
|
||||
|
||||
// Apply dark theme to containers
|
||||
const containers = ['jobs-container', 'configs-container'];
|
||||
containers.forEach(function(id) {
|
||||
const container = document.getElementById(id);
|
||||
if (container) {
|
||||
container.classList.add('dark');
|
||||
container.style.backgroundColor = '#111827';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body class="min-h-full bg-gray-50 dark:bg-gray-900" style="min-height: 100vh; display: flex; flex-direction: column;">
|
||||
<body class="min-h-full bg-gray-50 dark:bg-gray-900" style="min-height: 100vh; display: flex; flex-direction: column;" hx-on::after-swap="initFlowbite()">
|
||||
if isLoggedIn(ctx) {
|
||||
<!-- Application Shell -->
|
||||
<div class="flex min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
@@ -269,20 +238,100 @@ templ LayoutWithContext(title string, ctx context.Context) {
|
||||
<button data-drawer-target="mobile-menu" data-drawer-toggle="mobile-menu" class="p-2 text-gray-500 rounded-lg hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700">
|
||||
<i class="fas fa-bars text-lg"></i>
|
||||
</button>
|
||||
<!-- User menu (same as desktop) -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Add this drawer element for mobile menu -->
|
||||
<div id="mobile-menu" class="fixed top-0 left-0 z-40 h-screen p-4 overflow-y-auto transition-transform -translate-x-full bg-white w-64 dark:bg-gray-800" tabindex="-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<a href="/" class="flex items-center text-xl font-bold text-primary-600 dark:text-primary-400">
|
||||
<i class="fas fa-exchange-alt mr-2"></i>
|
||||
GoMFT
|
||||
</a>
|
||||
<button type="button" data-drawer-hide="mobile-menu" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Copy the same navigation items from the desktop sidebar -->
|
||||
<nav class="mt-5">
|
||||
<div class="space-y-1">
|
||||
<!-- Copy the same links from the desktop sidebar -->
|
||||
<a href="/dashboard" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||
<i class="fas fa-tachometer-alt mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||
Dashboard
|
||||
</a>
|
||||
<a href="/configs" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||
<i class="fas fa-cogs mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||
Configs
|
||||
</a>
|
||||
<a href="/jobs" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||
<i class="fas fa-tasks mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||
Jobs
|
||||
</a>
|
||||
<a href="/history" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||
<i class="fas fa-history mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||
History
|
||||
</a>
|
||||
<a href="/files" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||
<i class="fas fa-file-alt mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||
Files
|
||||
</a>
|
||||
if isAdmin(ctx) {
|
||||
<p class="px-3 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Administration
|
||||
</p>
|
||||
<a href="/admin/users" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||
<i class="fas fa-users w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||
Users
|
||||
</a>
|
||||
<a href="/admin/roles" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||
<i class="fas fa-user-shield w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||
Roles
|
||||
</a>
|
||||
<a href="/admin/audit" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||
<i class="fas fa-clipboard-list w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||
Audit Logs
|
||||
</a>
|
||||
<a href="/admin/database" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||
<i class="fas fa-database w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||
Database Tools
|
||||
</a>
|
||||
// Settings Dropdown
|
||||
<button type="button" class="flex items-center w-full p-2 text-base text-gray-900 transition duration-75 rounded-lg group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700" aria-controls="dropdown-settings" data-collapse-toggle="dropdown-settings">
|
||||
<i class="fas fa-cog w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||
<span class="flex-1 ms-3 text-left rtl:text-right whitespace-nowrap">Settings</span>
|
||||
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
|
||||
</svg>
|
||||
</button>
|
||||
<ul id="dropdown-settings" class="hidden py-2 space-y-2">
|
||||
// <li>
|
||||
// <a href="#" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">General (Coming Soon)</a>
|
||||
// </li>
|
||||
<li>
|
||||
<a href="/admin/settings/auth-providers" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||
<i class="fas fa-user-lock w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||
Authentication Providers
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/admin/settings/notifications" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||
<i class="fas fa-bell w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||
Notification Services
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
<!-- Main Content -->
|
||||
<div class="md:pl-64 flex flex-col flex-1 w-full bg-gray-50 dark:bg-gray-900">
|
||||
<!-- Main header -->
|
||||
<header class="bg-white dark:bg-gray-800 shadow-sm">
|
||||
<div class="flex items-center justify-between px-4 py-3 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center space-x-3">
|
||||
<button data-drawer-target="desktop-menu" data-drawer-toggle="desktop-menu" class="md:hidden p-2 text-gray-500 rounded-lg hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700">
|
||||
<i class="fas fa-bars text-lg"></i>
|
||||
</button>
|
||||
<h1 class="text-lg font-semibold text-gray-900 dark:text-white">{ title }</h1>
|
||||
</div>
|
||||
<!-- Theme toggle and user menu -->
|
||||
@@ -396,6 +445,8 @@ templ LayoutWithContext(title string, ctx context.Context) {
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Toast Container -->
|
||||
@toast.Container()
|
||||
<!-- Page Content -->
|
||||
<main class="flex-1 bg-gray-50 dark:bg-gray-900">
|
||||
<div class="py-6 bg-gray-50 dark:bg-gray-900">
|
||||
@@ -447,40 +498,14 @@ templ LayoutWithContext(title string, ctx context.Context) {
|
||||
</footer>
|
||||
</div>
|
||||
}
|
||||
<script>
|
||||
// Add script to ensure dark mode is properly applied
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Apply body dark class when theme changes
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
if (isDark) {
|
||||
document.body.classList.add('dark');
|
||||
|
||||
// Also apply to containers
|
||||
const jobsContainer = document.getElementById('jobs-container');
|
||||
const configsContainer = document.getElementById('configs-container');
|
||||
|
||||
if (jobsContainer) jobsContainer.classList.add('dark');
|
||||
if (configsContainer) configsContainer.classList.add('dark');
|
||||
}
|
||||
|
||||
// Initialize admin dropdown toggle if available
|
||||
const adminDropdownToggle = document.querySelector('[data-collapse-toggle="dropdown-settings"]');
|
||||
const adminDropdown = document.getElementById('dropdown-settings');
|
||||
|
||||
if (adminDropdownToggle && adminDropdown) {
|
||||
// Check if we should show the dropdown (if current page is under admin section)
|
||||
const currentPath = window.location.pathname;
|
||||
if (currentPath.startsWith('/admin')) {
|
||||
adminDropdown.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Add click event listener
|
||||
adminDropdownToggle.addEventListener('click', function() {
|
||||
adminDropdown.classList.toggle('hidden');
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<!-- Scripts -->
|
||||
<!-- Alpine.js and dependencies -->
|
||||
<script defer src="/static/dist/vendor.js"></script>
|
||||
<!-- Shared Scripts -->
|
||||
@toast.ShowToastJS()
|
||||
<!-- Application scripts -->
|
||||
<script defer src="/static/dist/app.js"></script>
|
||||
<script defer src="/static/dist/init.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
@@ -7,7 +7,10 @@ import (
|
||||
|
||||
// Dialog component for confirmation dialogs using Flowbite modal
|
||||
templ NotificationDialog(id string, title string, message string, confirmClass string, confirmText string, action string, serviceID uint, serviceName 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 id={ id } tabindex="-1" aria-hidden="true" class="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">
|
||||
<!-- Backdrop -->
|
||||
<div id={ fmt.Sprintf("%s-backdrop", id) } class="fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm"></div>
|
||||
<!-- Modal content -->
|
||||
<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">
|
||||
@@ -28,7 +31,7 @@ templ NotificationDialog(id string, title string, message string, confirmClass s
|
||||
onclick={ triggerServiceDelete(id, serviceID, serviceName) }>
|
||||
{ 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">
|
||||
<button type="button" onclick={ closeModal(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,16 +40,6 @@ templ NotificationDialog(id string, title string, message string, confirmClass s
|
||||
</div>
|
||||
}
|
||||
|
||||
script hideNotificationDialog(id string) {
|
||||
document.getElementById(id).classList.add("hidden");
|
||||
document.getElementById(id).classList.remove("flex");
|
||||
}
|
||||
|
||||
script showNotificationDialog(id string) {
|
||||
document.getElementById(id).classList.remove("hidden");
|
||||
document.getElementById(id).classList.add("flex");
|
||||
}
|
||||
|
||||
script triggerServiceDelete(dialogId string, serviceID uint, serviceName string) {
|
||||
// Hide the dialog
|
||||
document.getElementById(dialogId).classList.add("hidden");
|
||||
@@ -143,9 +136,7 @@ templ Notifications(ctx context.Context, data SettingsNotificationsData) {
|
||||
}
|
||||
|
||||
// Track all HTMX events for debugging
|
||||
document.addEventListener('htmx:beforeRequest', function(event) {
|
||||
console.log("HTMX before request:", event.detail);
|
||||
|
||||
document.addEventListener('htmx:beforeRequest', function(event) {
|
||||
// Check if this is a DELETE request for a notification service
|
||||
const path = event.detail.path;
|
||||
const method = event.detail.verb;
|
||||
@@ -161,9 +152,7 @@ templ Notifications(ctx context.Context, data SettingsNotificationsData) {
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('htmx:afterRequest', function(event) {
|
||||
console.log("HTMX after request:", event.detail);
|
||||
|
||||
document.addEventListener('htmx:afterRequest', function(event) {
|
||||
// Check for notification service deletion multiple ways
|
||||
const isDeleteRequest =
|
||||
// Check global flag from the triggerServiceDelete function
|
||||
@@ -369,7 +358,7 @@ templ Notifications(ctx context.Context, data SettingsNotificationsData) {
|
||||
)
|
||||
<button
|
||||
type="button"
|
||||
onclick={ showNotificationDialog(fmt.Sprintf("delete-notification-dialog-%d", service.ID)) }
|
||||
onclick={ showModal(fmt.Sprintf("delete-notification-dialog-%d", service.ID)) }
|
||||
class="text-red-500 bg-white focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 rounded-lg text-sm p-2 dark:bg-gray-800 dark:text-red-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"
|
||||
>
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package dialog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
// "strconv" // No longer needed here
|
||||
)
|
||||
|
||||
// NotificationDialog component for confirmation dialogs using Flowbite modal
|
||||
templ NotificationDialog(id string, title string, message string, confirmClass string, confirmText string, action string, serviceID uint, serviceName string) {
|
||||
<div id={ id } tabindex="-1" aria-hidden="true" class="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">
|
||||
<!-- Backdrop -->
|
||||
<div id={ fmt.Sprintf("%s-backdrop", id) } class="fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm"></div>
|
||||
<!-- Modal content -->
|
||||
<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={ confirmClass }
|
||||
hx-delete={ fmt.Sprintf("/admin/settings/notifications/%d", serviceID) }
|
||||
hx-target="body"
|
||||
data-service-name={ serviceName }
|
||||
data-service-id={ fmt.Sprint(serviceID) }
|
||||
id={ fmt.Sprintf("delete-btn-%d", serviceID) }
|
||||
onclick={ templ.ComponentScript{Call: fmt.Sprintf("triggerServiceDelete('%s', %d, '%s')", id, serviceID, serviceName)} }>
|
||||
{ confirmText }
|
||||
</button>
|
||||
<button type="button" onclick={ templ.ComponentScript{Call: fmt.Sprintf("closeModal('%s')", 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>
|
||||
}
|
||||
|
||||
// Scripts (triggerServiceDelete, closeModal, showModal) are now expected to be defined globally or in the calling template (e.g., list.templ).
|
||||
@@ -0,0 +1,20 @@
|
||||
package dialog
|
||||
|
||||
// DialogScripts provides the JavaScript function specific to the notification delete confirmation dialog.
|
||||
templ DialogScripts() {
|
||||
<script type="text/javascript">
|
||||
// Called when the delete confirmation button is clicked.
|
||||
// Primarily closes the modal; the actual delete is handled by hx-delete.
|
||||
function triggerServiceDelete(dialogId, serviceId, serviceName) {
|
||||
console.log(`Confirmed delete for service: ${serviceName} (ID: ${serviceId}). Closing modal: ${dialogId}`);
|
||||
// Call the global closeModal function defined elsewhere (e.g., app.js)
|
||||
if (typeof closeModal === 'function') {
|
||||
closeModal(dialogId);
|
||||
} else {
|
||||
console.error('Global closeModal function not found.');
|
||||
}
|
||||
// Optional: Show a "Deleting..." toast here if desired.
|
||||
// The hx-delete attribute on the button will trigger the actual backend request.
|
||||
}
|
||||
</script>
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package dialog
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
// DialogScripts provides the JavaScript function specific to the notification delete confirmation dialog.
|
||||
func DialogScripts() 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<script type=\"text/javascript\">\n\t\t// Called when the delete confirmation button is clicked.\n\t\t// Primarily closes the modal; the actual delete is handled by hx-delete.\n\t\tfunction triggerServiceDelete(dialogId, serviceId, serviceName) {\n\t\t\tconsole.log(`Confirmed delete for service: ${serviceName} (ID: ${serviceId}). Closing modal: ${dialogId}`);\n\t\t\t// Call the global closeModal function defined elsewhere (e.g., app.js)\n\t\t\tif (typeof closeModal === 'function') {\n\t\t\t\tcloseModal(dialogId);\n\t\t\t} else {\n\t\t\t\tconsole.error('Global closeModal function not found.');\n\t\t\t}\n\t\t\t// Optional: Show a \"Deleting...\" toast here if desired.\n\t\t\t// The hx-delete attribute on the button will trigger the actual backend request.\n\t\t}\n\t</script>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,218 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package dialog
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
// "strconv" // No longer needed here
|
||||
)
|
||||
|
||||
// NotificationDialog component for confirmation dialogs using Flowbite modal
|
||||
func NotificationDialog(id string, title string, message string, confirmClass string, confirmText string, action string, serviceID uint, serviceName string) 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(id)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/dialog/dialog.templ`, Line: 10, Col: 13}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" tabindex=\"-1\" aria-hidden=\"true\" class=\"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\"><!-- Backdrop --><div id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s-backdrop", id))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/dialog/dialog.templ`, Line: 12, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" class=\"fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm\"></div><!-- Modal content --><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 templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if action == "delete" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<i class=\"fas fa-trash-alt text-red-400 text-3xl mb-4\"></i>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<i class=\"fas fa-exclamation-triangle text-yellow-400 text-3xl mb-4\"></i>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<h3 class=\"mb-5 text-lg font-normal text-gray-500 dark:text-gray-400\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(message)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/dialog/dialog.templ`, Line: 22, Col: 84}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</h3>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 = []any{confirmClass}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var5...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("triggerServiceDelete('%s', %d, '%s')", id, serviceID, serviceName)})
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<button type=\"button\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var5).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/dialog/dialog.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" hx-delete=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("/admin/settings/notifications/%d", serviceID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/dialog/dialog.templ`, Line: 26, Col: 76}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" hx-target=\"body\" data-service-name=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(serviceName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/dialog/dialog.templ`, Line: 28, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" data-service-id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(serviceID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/dialog/dialog.templ`, Line: 29, Col: 45}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("delete-btn-%d", serviceID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/dialog/dialog.templ`, Line: 30, Col: 50}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" onclick=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("triggerServiceDelete('%s', %d, '%s')", id, serviceID, serviceName)}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11.Call)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(confirmText)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/dialog/dialog.templ`, Line: 32, Col: 19}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</button> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("closeModal('%s')", id)})
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<button type=\"button\" onclick=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("closeModal('%s')", id)}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13.Call)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" 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>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Scripts (triggerServiceDelete, closeModal, showModal) are now expected to be defined globally or in the calling template (e.g., list.templ).
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,32 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||
// No utils needed for this specific template yet
|
||||
)
|
||||
|
||||
templ EmailFields(data types.NotificationFormData) {
|
||||
<!-- TODO: Populate value attributes if editing an email service -->
|
||||
<div id="email_fields" class="hidden notification-fields">
|
||||
<div class="mb-6">
|
||||
<label for="smtp_host" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">SMTP Host</label>
|
||||
<input type="text" id="smtp_host" name="smtp_host" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="smtp.example.com"/>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="smtp_port" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">SMTP Port</label>
|
||||
<input type="number" id="smtp_port" name="smtp_port" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="587"/>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="smtp_username" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">SMTP Username</label>
|
||||
<input type="text" id="smtp_username" name="smtp_username" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="user@example.com"/>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="smtp_password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">SMTP Password</label>
|
||||
<input type="password" id="smtp_password" name="smtp_password" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"/>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="from_email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">From Email</label>
|
||||
<input type="email" id="from_email" name="from_email" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="notifications@example.com"/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package fields
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||
// No utils needed for this specific template yet
|
||||
)
|
||||
|
||||
func EmailFields(data types.NotificationFormData) 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!-- TODO: Populate value attributes if editing an email service --><div id=\"email_fields\" class=\"hidden notification-fields\"><div class=\"mb-6\"><label for=\"smtp_host\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">SMTP Host</label> <input type=\"text\" id=\"smtp_host\" name=\"smtp_host\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"smtp.example.com\"></div><div class=\"mb-6\"><label for=\"smtp_port\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">SMTP Port</label> <input type=\"number\" id=\"smtp_port\" name=\"smtp_port\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"587\"></div><div class=\"mb-6\"><label for=\"smtp_username\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">SMTP Username</label> <input type=\"text\" id=\"smtp_username\" name=\"smtp_username\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"user@example.com\"></div><div class=\"mb-6\"><label for=\"smtp_password\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">SMTP Password</label> <input type=\"password\" id=\"smtp_password\" name=\"smtp_password\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"></div><div class=\"mb-6\"><label for=\"from_email\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">From Email</label> <input type=\"email\" id=\"from_email\" name=\"from_email\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"notifications@example.com\"></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,88 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||
// "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
|
||||
)
|
||||
|
||||
templ GotifyFields(data types.NotificationFormData) {
|
||||
<div id="gotify_fields" class="hidden notification-fields">
|
||||
<div class="mb-6">
|
||||
<label for="gotify_url" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Gotify Server URL</label>
|
||||
if data.NotificationService.GotifyURL != "" {
|
||||
<input type="url" id="gotify_url" name="gotify_url" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="https://gotify.example.com" value={ data.NotificationService.GotifyURL }/>
|
||||
} else {
|
||||
<input type="url" id="gotify_url" name="gotify_url" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="https://gotify.example.com" value=""/>
|
||||
}
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">URL of your Gotify server</p>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="gotify_token" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Application Token</label>
|
||||
if data.NotificationService.GotifyToken != "" {
|
||||
<input type="text" id="gotify_token" name="gotify_token" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="A-M-XiEQj.zX5d" value={ data.NotificationService.GotifyToken }/>
|
||||
} else {
|
||||
<input type="text" id="gotify_token" name="gotify_token" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="A-M-XiEQj.zX5d" value=""/>
|
||||
}
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Find this in your Gotify application settings</p>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="gotify_priority" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Default Priority</label>
|
||||
<select id="gotify_priority" name="gotify_priority" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||
<option value="0">Low (0)</option>
|
||||
if data.NotificationService.GotifyPriority != "" && data.NotificationService.GotifyPriority == "5" {
|
||||
<option value="5" selected="selected">Normal (5)</option>
|
||||
} else {
|
||||
<option value="5">Normal (5)</option>
|
||||
}
|
||||
<option value="8">High (8)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="gotify_title_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message Title Template</label>
|
||||
if data.NotificationService.GotifyTitleTemplate != "" {
|
||||
<input type="text" id="gotify_title_template" name="gotify_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" value={ data.NotificationService.GotifyTitleTemplate }/>
|
||||
} else {
|
||||
<input type="text" id="gotify_title_template" name="gotify_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="{{job.name}} {{job.status}}" value=""/>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="gotify_message_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message Body Template</label>
|
||||
<textarea
|
||||
id="gotify_message_template"
|
||||
name="gotify_message_template"
|
||||
rows="4"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes)."
|
||||
>
|
||||
if data.NotificationService.GotifyMessageTemplate != "" {
|
||||
data.NotificationService.GotifyMessageTemplate
|
||||
}</textarea>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p>
|
||||
</div>
|
||||
// Removed duplicate Event Triggers section - now handled in form.templ
|
||||
<!-- Test notification button for Gotify -->
|
||||
<div class="mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h4 class="text-base font-medium text-gray-900 dark:text-white">Test Configuration</h4>
|
||||
<button
|
||||
type="button"
|
||||
id="test-gotify-btn"
|
||||
hx-post="/admin/settings/notifications/test"
|
||||
hx-trigger="click"
|
||||
hx-target="#test-notification-result"
|
||||
hx-swap="outerHTML"
|
||||
class="px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
||||
>
|
||||
<i class="fas fa-paper-plane mr-1"></i>
|
||||
Send Test Notification
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Send a test notification to verify your Gotify configuration works correctly before saving.
|
||||
</p>
|
||||
<div id="test-notification-result" class="mt-3 hidden">
|
||||
<!-- Result will be shown here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package fields
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||
// "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
|
||||
)
|
||||
|
||||
func GotifyFields(data types.NotificationFormData) 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"gotify_fields\" class=\"hidden notification-fields\"><div class=\"mb-6\"><label for=\"gotify_url\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Gotify Server URL</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.GotifyURL != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<input type=\"url\" id=\"gotify_url\" name=\"gotify_url\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"https://gotify.example.com\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.GotifyURL)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/gotify.templ`, Line: 13, Col: 407}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<input type=\"url\" id=\"gotify_url\" name=\"gotify_url\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"https://gotify.example.com\" value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">URL of your Gotify server</p></div><div class=\"mb-6\"><label for=\"gotify_token\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Application Token</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.GotifyToken != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<input type=\"text\" id=\"gotify_token\" name=\"gotify_token\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"A-M-XiEQj.zX5d\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.GotifyToken)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/gotify.templ`, Line: 22, Col: 402}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<input type=\"text\" id=\"gotify_token\" name=\"gotify_token\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"A-M-XiEQj.zX5d\" value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Find this in your Gotify application settings</p></div><div class=\"mb-6\"><label for=\"gotify_priority\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Default Priority</label> <select id=\"gotify_priority\" name=\"gotify_priority\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"><option value=\"0\">Low (0)</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.GotifyPriority != "" && data.NotificationService.GotifyPriority == "5" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<option value=\"5\" selected=\"selected\">Normal (5)</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<option value=\"5\">Normal (5)</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<option value=\"8\">High (8)</option></select></div><div class=\"mb-6\"><label for=\"gotify_title_template\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Message Title Template</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.GotifyTitleTemplate != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<input type=\"text\" id=\"gotify_title_template\" name=\"gotify_title_template\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.GotifyTitleTemplate)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/gotify.templ`, Line: 43, Col: 399}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<input type=\"text\" id=\"gotify_title_template\" name=\"gotify_title_template\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"{{job.name}} {{job.status}}\" value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div><div class=\"mb-6\"><label for=\"gotify_message_template\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Message Body Template</label> <textarea id=\"gotify_message_template\" name=\"gotify_message_template\" rows=\"4\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes).\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.GotifyMessageTemplate != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "data.NotificationService.GotifyMessageTemplate")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</textarea><p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p></div><!-- Test notification button for Gotify --><div class=\"mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600\"><div class=\"flex items-center justify-between mb-2\"><h4 class=\"text-base font-medium text-gray-900 dark:text-white\">Test Configuration</h4><button type=\"button\" id=\"test-gotify-btn\" hx-post=\"/admin/settings/notifications/test\" hx-trigger=\"click\" hx-target=\"#test-notification-result\" hx-swap=\"outerHTML\" class=\"px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-paper-plane mr-1\"></i> Send Test Notification</button></div><p class=\"text-sm text-gray-500 dark:text-gray-400\">Send a test notification to verify your Gotify configuration works correctly before saving.</p><div id=\"test-notification-result\" class=\"mt-3 hidden\"><!-- Result will be shown here --></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,105 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||
// "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
|
||||
)
|
||||
|
||||
templ NtfyFields(data types.NotificationFormData) {
|
||||
<div id="ntfy_fields" class="hidden notification-fields">
|
||||
<div class="mb-6">
|
||||
<label for="ntfy_server" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Ntfy Server</label>
|
||||
if data.NotificationService.NtfyServer != "" {
|
||||
<input type="url" id="ntfy_server" name="ntfy_server" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="https://ntfy.sh" value={ data.NotificationService.NtfyServer }/>
|
||||
} else {
|
||||
<input type="url" id="ntfy_server" name="ntfy_server" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="https://ntfy.sh" value="https://ntfy.sh"/>
|
||||
}
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">The Ntfy server URL (default: ntfy.sh)</p>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="ntfy_topic" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Topic</label>
|
||||
if data.NotificationService.NtfyTopic != "" {
|
||||
<input type="text" id="ntfy_topic" name="ntfy_topic" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="your-unique-topic" value={ data.NotificationService.NtfyTopic }/>
|
||||
} else {
|
||||
<input type="text" id="ntfy_topic" name="ntfy_topic" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="your-unique-topic" value="gomft"/>
|
||||
}
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Choose a unique, unguessable topic name</p>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="ntfy_priority" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Default Priority</label>
|
||||
<select id="ntfy_priority" name="ntfy_priority" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||
<option value="1">Low (1)</option>
|
||||
if data.NotificationService.NtfyPriority == "3" {
|
||||
<option value="3" selected="selected">Default (3)</option>
|
||||
} else {
|
||||
<option value="3">Default (3)</option>
|
||||
}
|
||||
<option value="4">High (4)</option>
|
||||
<option value="5">Urgent (5)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="ntfy_username" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Username (Optional)</label>
|
||||
if data.NotificationService.NtfyUsername != "" {
|
||||
<input type="text" id="ntfy_username" name="ntfy_username" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Username for protected topics" value={ data.NotificationService.NtfyUsername }/>
|
||||
} else {
|
||||
<input type="text" id="ntfy_username" name="ntfy_username" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Username for protected topics" value=""/>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="ntfy_password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password (Optional)</label>
|
||||
if data.NotificationService.NtfyPassword != "" {
|
||||
<input type="password" id="ntfy_password" name="ntfy_password" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Password for protected topics" value={ data.NotificationService.NtfyPassword }/>
|
||||
} else {
|
||||
<input type="password" id="ntfy_password" name="ntfy_password" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Password for protected topics" value=""/>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="ntfy_title_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Notification Title Template</label>
|
||||
if data.NotificationService.NtfyTitleTemplate != "" {
|
||||
<input type="text" id="ntfy_title_template" name="ntfy_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" value={ data.NotificationService.NtfyTitleTemplate }/>
|
||||
} else {
|
||||
<input type="text" id="ntfy_title_template" name="ntfy_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="{{job.name}} {{job.status}}" value=""/>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="ntfy_message_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message Body Template</label>
|
||||
<textarea
|
||||
id="ntfy_message_template"
|
||||
name="ntfy_message_template"
|
||||
rows="4"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes)."
|
||||
>
|
||||
if data.NotificationService.NtfyMessageTemplate != "" {
|
||||
data.NotificationService.NtfyMessageTemplate
|
||||
} </textarea>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p>
|
||||
</div>
|
||||
// Removed duplicate Event Triggers section - now handled in form.templ
|
||||
<!-- Test notification button for Ntfy -->
|
||||
<div class="mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h4 class="text-base font-medium text-gray-900 dark:text-white">Test Configuration</h4>
|
||||
<button
|
||||
type="button"
|
||||
id="test-ntfy-btn"
|
||||
hx-post="/admin/settings/notifications/test"
|
||||
hx-trigger="click"
|
||||
hx-target="#test-notification-result"
|
||||
hx-swap="outerHTML"
|
||||
class="px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
||||
>
|
||||
<i class="fas fa-paper-plane mr-1"></i>
|
||||
Send Test Notification
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Send a test notification to verify your Ntfy configuration works correctly before saving.
|
||||
</p>
|
||||
<div id="test-notification-result" class="mt-3 hidden">
|
||||
<!-- Result will be shown here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package fields
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||
// "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
|
||||
)
|
||||
|
||||
func NtfyFields(data types.NotificationFormData) 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"ntfy_fields\" class=\"hidden notification-fields\"><div class=\"mb-6\"><label for=\"ntfy_server\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Ntfy Server</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.NtfyServer != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<input type=\"url\" id=\"ntfy_server\" name=\"ntfy_server\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"https://ntfy.sh\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.NtfyServer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/ntfy.templ`, Line: 13, Col: 399}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<input type=\"url\" id=\"ntfy_server\" name=\"ntfy_server\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"https://ntfy.sh\" value=\"https://ntfy.sh\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">The Ntfy server URL (default: ntfy.sh)</p></div><div class=\"mb-6\"><label for=\"ntfy_topic\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Topic</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.NtfyTopic != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<input type=\"text\" id=\"ntfy_topic\" name=\"ntfy_topic\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"your-unique-topic\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.NtfyTopic)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/ntfy.templ`, Line: 22, Col: 399}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<input type=\"text\" id=\"ntfy_topic\" name=\"ntfy_topic\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"your-unique-topic\" value=\"gomft\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Choose a unique, unguessable topic name</p></div><div class=\"mb-6\"><label for=\"ntfy_priority\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Default Priority</label> <select id=\"ntfy_priority\" name=\"ntfy_priority\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"><option value=\"1\">Low (1)</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.NtfyPriority == "3" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<option value=\"3\" selected=\"selected\">Default (3)</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<option value=\"3\">Default (3)</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<option value=\"4\">High (4)</option> <option value=\"5\">Urgent (5)</option></select></div><div class=\"mb-6\"><label for=\"ntfy_username\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Username (Optional)</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.NtfyUsername != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<input type=\"text\" id=\"ntfy_username\" name=\"ntfy_username\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"Username for protected topics\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.NtfyUsername)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/ntfy.templ`, Line: 44, Col: 420}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<input type=\"text\" id=\"ntfy_username\" name=\"ntfy_username\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"Username for protected topics\" value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div><div class=\"mb-6\"><label for=\"ntfy_password\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Password (Optional)</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.NtfyPassword != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<input type=\"password\" id=\"ntfy_password\" name=\"ntfy_password\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"Password for protected topics\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.NtfyPassword)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/ntfy.templ`, Line: 52, Col: 424}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<input type=\"password\" id=\"ntfy_password\" name=\"ntfy_password\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"Password for protected topics\" value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div><div class=\"mb-6\"><label for=\"ntfy_title_template\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Notification Title Template</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.NtfyTitleTemplate != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<input type=\"text\" id=\"ntfy_title_template\" name=\"ntfy_title_template\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.NtfyTitleTemplate)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/ntfy.templ`, Line: 60, Col: 393}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<input type=\"text\" id=\"ntfy_title_template\" name=\"ntfy_title_template\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"{{job.name}} {{job.status}}\" value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</div><div class=\"mb-6\"><label for=\"ntfy_message_template\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Message Body Template</label> <textarea id=\"ntfy_message_template\" name=\"ntfy_message_template\" rows=\"4\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes).\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.NtfyMessageTemplate != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "data.NotificationService.NtfyMessageTemplate")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</textarea><p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p></div><!-- Test notification button for Ntfy --><div class=\"mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600\"><div class=\"flex items-center justify-between mb-2\"><h4 class=\"text-base font-medium text-gray-900 dark:text-white\">Test Configuration</h4><button type=\"button\" id=\"test-ntfy-btn\" hx-post=\"/admin/settings/notifications/test\" hx-trigger=\"click\" hx-target=\"#test-notification-result\" hx-swap=\"outerHTML\" class=\"px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-paper-plane mr-1\"></i> Send Test Notification</button></div><p class=\"text-sm text-gray-500 dark:text-gray-400\">Send a test notification to verify your Ntfy configuration works correctly before saving.</p><div id=\"test-notification-result\" class=\"mt-3 hidden\"><!-- Result will be shown here --></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,75 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||
// "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
|
||||
)
|
||||
|
||||
templ PushbulletFields(data types.NotificationFormData) {
|
||||
<div id="pushbullet_fields" class="hidden notification-fields">
|
||||
<div class="mb-6">
|
||||
<label for="pushbullet_api_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">API Key</label>
|
||||
if data.NotificationService.PushbulletAPIKey != "" {
|
||||
<input type="text" id="pushbullet_api_key" name="pushbullet_api_key" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="o.XyzAbCdEfGhIjKlMnOpQrSt" value={ data.NotificationService.PushbulletAPIKey }/>
|
||||
} else {
|
||||
<input type="text" id="pushbullet_api_key" name="pushbullet_api_key" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="o.XyzAbCdEfGhIjKlMnOpQrSt" value=""/>
|
||||
}
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Get your API key from <a href="https://www.pushbullet.com/#settings/account" target="_blank" class="text-blue-500 hover:underline">Pushbullet Account Settings</a></p>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="pushbullet_device_iden" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Device Identifier (Optional)</label>
|
||||
if data.NotificationService.PushbulletDeviceID != "" {
|
||||
<input type="text" id="pushbullet_device_iden" name="pushbullet_device_iden" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Leave empty to send to all devices" value={ data.NotificationService.PushbulletDeviceID }/>
|
||||
} else {
|
||||
<input type="text" id="pushbullet_device_iden" name="pushbullet_device_iden" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Leave empty to send to all devices" value=""/>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="pushbullet_title_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Notification Title Template</label>
|
||||
if data.NotificationService.PushbulletTitleTemplate != "" {
|
||||
<input type="text" id="pushbullet_title_template" name="pushbullet_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" value={ data.NotificationService.PushbulletTitleTemplate }/>
|
||||
} else {
|
||||
<input type="text" id="pushbullet_title_template" name="pushbullet_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="{{job.name}} {{job.status}}" value=""/>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="pushbullet_body_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message Body Template</label>
|
||||
<textarea
|
||||
id="pushbullet_body_template"
|
||||
name="pushbullet_body_template"
|
||||
rows="4"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes)."
|
||||
>
|
||||
if data.NotificationService.PushbulletBodyTemplate != "" {
|
||||
data.NotificationService.PushbulletBodyTemplate
|
||||
} </textarea>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p>
|
||||
</div>
|
||||
// Removed duplicate Event Triggers section - now handled in form.templ
|
||||
<!-- Test notification button for Pushbullet -->
|
||||
<div class="mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h4 class="text-base font-medium text-gray-900 dark:text-white">Test Configuration</h4>
|
||||
<button
|
||||
type="button"
|
||||
id="test-pushbullet-btn"
|
||||
hx-post="/admin/settings/notifications/test"
|
||||
hx-trigger="click"
|
||||
hx-target="#test-notification-result"
|
||||
hx-swap="outerHTML"
|
||||
class="px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
||||
>
|
||||
<i class="fas fa-paper-plane mr-1"></i>
|
||||
Send Test Notification
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Send a test notification to verify your Pushbullet configuration works correctly before saving.
|
||||
</p>
|
||||
<div id="test-notification-result" class="mt-3 hidden">
|
||||
<!-- Result will be shown here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package fields
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||
// "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
|
||||
)
|
||||
|
||||
func PushbulletFields(data types.NotificationFormData) 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"pushbullet_fields\" class=\"hidden notification-fields\"><div class=\"mb-6\"><label for=\"pushbullet_api_key\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">API Key</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.PushbulletAPIKey != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<input type=\"text\" id=\"pushbullet_api_key\" name=\"pushbullet_api_key\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"o.XyzAbCdEfGhIjKlMnOpQrSt\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.PushbulletAPIKey)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/pushbullet.templ`, Line: 13, Col: 430}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<input type=\"text\" id=\"pushbullet_api_key\" name=\"pushbullet_api_key\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"o.XyzAbCdEfGhIjKlMnOpQrSt\" value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Get your API key from <a href=\"https://www.pushbullet.com/#settings/account\" target=\"_blank\" class=\"text-blue-500 hover:underline\">Pushbullet Account Settings</a></p></div><div class=\"mb-6\"><label for=\"pushbullet_device_iden\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Device Identifier (Optional)</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.PushbulletDeviceID != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<input type=\"text\" id=\"pushbullet_device_iden\" name=\"pushbullet_device_iden\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"Leave empty to send to all devices\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.PushbulletDeviceID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/pushbullet.templ`, Line: 22, Col: 449}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<input type=\"text\" id=\"pushbullet_device_iden\" name=\"pushbullet_device_iden\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"Leave empty to send to all devices\" value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div><div class=\"mb-6\"><label for=\"pushbullet_title_template\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Notification Title Template</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.PushbulletTitleTemplate != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<input type=\"text\" id=\"pushbullet_title_template\" name=\"pushbullet_title_template\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.PushbulletTitleTemplate)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/pushbullet.templ`, Line: 30, Col: 411}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<input type=\"text\" id=\"pushbullet_title_template\" name=\"pushbullet_title_template\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"{{job.name}} {{job.status}}\" value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div><div class=\"mb-6\"><label for=\"pushbullet_body_template\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Message Body Template</label> <textarea id=\"pushbullet_body_template\" name=\"pushbullet_body_template\" rows=\"4\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes).\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.PushbulletBodyTemplate != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "data.NotificationService.PushbulletBodyTemplate")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</textarea><p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p></div><!-- Test notification button for Pushbullet --><div class=\"mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600\"><div class=\"flex items-center justify-between mb-2\"><h4 class=\"text-base font-medium text-gray-900 dark:text-white\">Test Configuration</h4><button type=\"button\" id=\"test-pushbullet-btn\" hx-post=\"/admin/settings/notifications/test\" hx-trigger=\"click\" hx-target=\"#test-notification-result\" hx-swap=\"outerHTML\" class=\"px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-paper-plane mr-1\"></i> Send Test Notification</button></div><p class=\"text-sm text-gray-500 dark:text-gray-400\">Send a test notification to verify your Pushbullet configuration works correctly before saving.</p><div id=\"test-notification-result\" class=\"mt-3 hidden\"><!-- Result will be shown here --></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,126 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||
// "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
|
||||
)
|
||||
|
||||
templ PushoverFields(data types.NotificationFormData) {
|
||||
<div id="pushover_fields" class="hidden notification-fields">
|
||||
<div class="mb-6">
|
||||
<label for="pushover_app_token" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">API Token/Key</label>
|
||||
if data.NotificationService.PushoverAPIToken != "" {
|
||||
<input type="text" id="pushover_app_token" name="pushover_app_token" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="azGDORePK8gMaC0QOYAMyEEuzJnyUi" value={ data.NotificationService.PushoverAPIToken }/>
|
||||
} else {
|
||||
<input type="text" id="pushover_app_token" name="pushover_app_token" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="azGDORePK8gMaC0QOYAMyEEuzJnyUi" value=""/>
|
||||
}
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Your application's API token/key from <a href="https://pushover.net/apps" target="_blank" class="text-blue-500 hover:underline">Pushover Dashboard</a></p>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="pushover_user_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">User Key</label>
|
||||
if data.NotificationService.PushoverUserKey != "" {
|
||||
<input type="text" id="pushover_user_key" name="pushover_user_key" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="uQiRzpo4DXghDmr9QzzfQu27cmVRsG" value={ data.NotificationService.PushoverUserKey }/>
|
||||
} else {
|
||||
<input type="text" id="pushover_user_key" name="pushover_user_key" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="uQiRzpo4DXghDmr9QzzfQu27cmVRsG" value=""/>
|
||||
}
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Your user key from <a href="https://pushover.net/" target="_blank" class="text-blue-500 hover:underline">Pushover Dashboard</a></p>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="pushover_device" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Device Name (Optional)</label>
|
||||
if data.NotificationService.PushoverDevice != "" {
|
||||
<input type="text" id="pushover_device" name="pushover_device" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Leave empty to send to all devices" value={ data.NotificationService.PushoverDevice }/>
|
||||
} else {
|
||||
<input type="text" id="pushover_device" name="pushover_device" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Leave empty to send to all devices" value=""/>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="pushover_priority" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Default Priority</label>
|
||||
<select id="pushover_priority" name="pushover_priority" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||
<option value="-2">Lowest (-2)</option>
|
||||
<option value="-1">Low (-1)</option>
|
||||
if data.NotificationService.PushoverPriority != "" && data.NotificationService.PushoverPriority == "0" {
|
||||
<option value="0" selected="selected">Normal (0)</option>
|
||||
} else {
|
||||
<option value="0">Normal (0)</option>
|
||||
}
|
||||
<option value="1">High (1)</option>
|
||||
<option value="2">Emergency (2)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="pushover_sound" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Sound</label>
|
||||
<select id="pushover_sound" name="pushover_sound" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||
<option value="pushover">Pushover (default)</option>
|
||||
<option value="bike">Bike</option>
|
||||
<option value="bugle">Bugle</option>
|
||||
<option value="cashregister">Cash Register</option>
|
||||
<option value="classical">Classical</option>
|
||||
<option value="cosmic">Cosmic</option>
|
||||
<option value="falling">Falling</option>
|
||||
<option value="gamelan">Gamelan</option>
|
||||
<option value="incoming">Incoming</option>
|
||||
<option value="intermission">Intermission</option>
|
||||
<option value="magic">Magic</option>
|
||||
<option value="mechanical">Mechanical</option>
|
||||
<option value="pianobar">Piano Bar</option>
|
||||
<option value="siren">Siren</option>
|
||||
<option value="spacealarm">Space Alarm</option>
|
||||
<option value="tugboat">Tug Boat</option>
|
||||
<option value="alien">Alien Alarm (long)</option>
|
||||
<option value="climb">Climb (long)</option>
|
||||
<option value="persistent">Persistent (long)</option>
|
||||
<option value="echo">Echo (long)</option>
|
||||
<option value="updown">Up Down (long)</option>
|
||||
<option value="vibrate">Vibrate Only</option>
|
||||
<option value="none">None (silent)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="pushover_title_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message Title Template</label>
|
||||
if data.NotificationService.PushoverTitleTemplate != "" {
|
||||
<input type="text" id="pushover_title_template" name="pushover_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" value={ data.NotificationService.PushoverTitleTemplate }/>
|
||||
} else {
|
||||
<input type="text" id="pushover_title_template" name="pushover_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="'{{job.name}}' {{job.status}}" value=""/>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="pushover_message_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message Body Template</label>
|
||||
<textarea
|
||||
id="pushover_message_template"
|
||||
name="pushover_message_template"
|
||||
rows="4"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes)."
|
||||
>
|
||||
if data.NotificationService.PushoverMessageTemplate != "" {
|
||||
data.NotificationService.PushoverMessageTemplate
|
||||
} </textarea>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p>
|
||||
</div>
|
||||
// Removed duplicate Event Triggers section - now handled in form.templ
|
||||
<!-- Test notification button for Pushover -->
|
||||
<div class="mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h4 class="text-base font-medium text-gray-900 dark:text-white">Test Configuration</h4>
|
||||
<button
|
||||
type="button"
|
||||
id="test-pushover-btn"
|
||||
hx-post="/admin/settings/notifications/test"
|
||||
hx-trigger="click"
|
||||
hx-target="#test-notification-result"
|
||||
hx-swap="outerHTML"
|
||||
class="px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
||||
>
|
||||
<i class="fas fa-paper-plane mr-1"></i>
|
||||
Send Test Notification
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Send a test notification to verify your Pushover configuration works correctly before saving.
|
||||
</p>
|
||||
<div id="test-notification-result" class="mt-3 hidden">
|
||||
<!-- Result will be shown here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package fields
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||
// "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
|
||||
)
|
||||
|
||||
func PushoverFields(data types.NotificationFormData) 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"pushover_fields\" class=\"hidden notification-fields\"><div class=\"mb-6\"><label for=\"pushover_app_token\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">API Token/Key</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.PushoverAPIToken != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<input type=\"text\" id=\"pushover_app_token\" name=\"pushover_app_token\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"azGDORePK8gMaC0QOYAMyEEuzJnyUi\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.PushoverAPIToken)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/pushover.templ`, Line: 13, Col: 435}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<input type=\"text\" id=\"pushover_app_token\" name=\"pushover_app_token\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"azGDORePK8gMaC0QOYAMyEEuzJnyUi\" value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Your application's API token/key from <a href=\"https://pushover.net/apps\" target=\"_blank\" class=\"text-blue-500 hover:underline\">Pushover Dashboard</a></p></div><div class=\"mb-6\"><label for=\"pushover_user_key\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">User Key</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.PushoverUserKey != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<input type=\"text\" id=\"pushover_user_key\" name=\"pushover_user_key\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"uQiRzpo4DXghDmr9QzzfQu27cmVRsG\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.PushoverUserKey)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/pushover.templ`, Line: 22, Col: 432}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<input type=\"text\" id=\"pushover_user_key\" name=\"pushover_user_key\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"uQiRzpo4DXghDmr9QzzfQu27cmVRsG\" value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Your user key from <a href=\"https://pushover.net/\" target=\"_blank\" class=\"text-blue-500 hover:underline\">Pushover Dashboard</a></p></div><div class=\"mb-6\"><label for=\"pushover_device\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Device Name (Optional)</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.PushoverDevice != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<input type=\"text\" id=\"pushover_device\" name=\"pushover_device\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"Leave empty to send to all devices\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.PushoverDevice)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/pushover.templ`, Line: 31, Col: 431}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<input type=\"text\" id=\"pushover_device\" name=\"pushover_device\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"Leave empty to send to all devices\" value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div><div class=\"mb-6\"><label for=\"pushover_priority\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Default Priority</label> <select id=\"pushover_priority\" name=\"pushover_priority\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"><option value=\"-2\">Lowest (-2)</option> <option value=\"-1\">Low (-1)</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.PushoverPriority != "" && data.NotificationService.PushoverPriority == "0" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<option value=\"0\" selected=\"selected\">Normal (0)</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<option value=\"0\">Normal (0)</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<option value=\"1\">High (1)</option> <option value=\"2\">Emergency (2)</option></select></div><div class=\"mb-6\"><label for=\"pushover_sound\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Sound</label> <select id=\"pushover_sound\" name=\"pushover_sound\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"><option value=\"pushover\">Pushover (default)</option> <option value=\"bike\">Bike</option> <option value=\"bugle\">Bugle</option> <option value=\"cashregister\">Cash Register</option> <option value=\"classical\">Classical</option> <option value=\"cosmic\">Cosmic</option> <option value=\"falling\">Falling</option> <option value=\"gamelan\">Gamelan</option> <option value=\"incoming\">Incoming</option> <option value=\"intermission\">Intermission</option> <option value=\"magic\">Magic</option> <option value=\"mechanical\">Mechanical</option> <option value=\"pianobar\">Piano Bar</option> <option value=\"siren\">Siren</option> <option value=\"spacealarm\">Space Alarm</option> <option value=\"tugboat\">Tug Boat</option> <option value=\"alien\">Alien Alarm (long)</option> <option value=\"climb\">Climb (long)</option> <option value=\"persistent\">Persistent (long)</option> <option value=\"echo\">Echo (long)</option> <option value=\"updown\">Up Down (long)</option> <option value=\"vibrate\">Vibrate Only</option> <option value=\"none\">None (silent)</option></select></div><div class=\"mb-6\"><label for=\"pushover_title_template\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Message Title Template</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.PushoverTitleTemplate != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<input type=\"text\" id=\"pushover_title_template\" name=\"pushover_title_template\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.PushoverTitleTemplate)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/pushover.templ`, Line: 81, Col: 405}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<input type=\"text\" id=\"pushover_title_template\" name=\"pushover_title_template\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"'{{job.name}}' {{job.status}}\" value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div><div class=\"mb-6\"><label for=\"pushover_message_template\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Message Body Template</label> <textarea id=\"pushover_message_template\" name=\"pushover_message_template\" rows=\"4\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes).\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.PushoverMessageTemplate != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "data.NotificationService.PushoverMessageTemplate")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</textarea><p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p></div><!-- Test notification button for Pushover --><div class=\"mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600\"><div class=\"flex items-center justify-between mb-2\"><h4 class=\"text-base font-medium text-gray-900 dark:text-white\">Test Configuration</h4><button type=\"button\" id=\"test-pushover-btn\" hx-post=\"/admin/settings/notifications/test\" hx-trigger=\"click\" hx-target=\"#test-notification-result\" hx-swap=\"outerHTML\" class=\"px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-paper-plane mr-1\"></i> Send Test Notification</button></div><p class=\"text-sm text-gray-500 dark:text-gray-400\">Send a test notification to verify your Pushover configuration works correctly before saving.</p><div id=\"test-notification-result\" class=\"mt-3 hidden\"><!-- Result will be shown here --></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,142 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||
// "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
|
||||
)
|
||||
|
||||
templ WebhookFields(data types.NotificationFormData) {
|
||||
<div id="webhook_fields" class="hidden notification-fields">
|
||||
<div class="mb-6">
|
||||
<label for="webhook_url" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Webhook URL</label>
|
||||
if data.NotificationService.WebhookURL != "" {
|
||||
<input type="url" id="webhook_url" name="webhook_url" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="https://api.example.com/webhook" value={ data.NotificationService.WebhookURL }/>
|
||||
} else {
|
||||
<input type="url" id="webhook_url" name="webhook_url" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="https://api.example.com/webhook" value=""/>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="method" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">HTTP Method</label>
|
||||
<select id="method" name="method" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||
if data.NotificationService.Method != "" {
|
||||
if data.NotificationService.Method == "POST" {
|
||||
<option value="POST" selected="selected">POST</option>
|
||||
} else {
|
||||
<option value="POST">POST</option>
|
||||
}
|
||||
if data.NotificationService.Method == "PUT" {
|
||||
<option value="PUT" selected="selected">PUT</option>
|
||||
} else {
|
||||
<option value="PUT">PUT</option>
|
||||
}
|
||||
} else {
|
||||
<option value="POST">POST</option>
|
||||
<option value="PUT">PUT</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="headers" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Headers (JSON)</label>
|
||||
if data.NotificationService.Headers != "" {
|
||||
<textarea id="headers" name="headers" rows="3" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder='{"Content-Type": "application/json", "Authorization": "Bearer token"}'>{ data.NotificationService.Headers }</textarea>
|
||||
} else {
|
||||
<textarea id="headers" name="headers" rows="3" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder='{"Content-Type": "application/json", "Authorization": "Bearer token"}'></textarea>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="payload_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Payload Template (JSON)</label>
|
||||
<textarea
|
||||
id="payload_template"
|
||||
name="payload_template"
|
||||
rows="5"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder='{
|
||||
"event": "{{job.event}}",
|
||||
"job": {
|
||||
"id": "{{job.id}}",
|
||||
"name": "{{job.name}}",
|
||||
"status": "{{job.status}}",
|
||||
"message": "{{job.message}}",
|
||||
"started_at": "{{job.started_at}}",
|
||||
"completed_at": "{{job.completed_at}}",
|
||||
"duration_seconds": {{job.duration_seconds}},
|
||||
"config_id": "{{job.config_id}}",
|
||||
"config_name": "{{job.config_name}}",
|
||||
"transfer_bytes": {{job.transfer_bytes}},
|
||||
"file_count": {{job.file_count}}
|
||||
},
|
||||
"instance": {
|
||||
"id": "{{instance.id}}",
|
||||
"name": "{{instance.name}}",
|
||||
"version": "{{instance.version}}",
|
||||
"environment": "{{instance.environment}}"
|
||||
},
|
||||
"timestamp": "{{timestamp}}",
|
||||
"notification_id": "{{notification.id}}"
|
||||
}'
|
||||
>
|
||||
if data.NotificationService.PayloadTemplate != "" {
|
||||
data.NotificationService.PayloadTemplate
|
||||
} </textarea>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p>
|
||||
</div>
|
||||
// Removed duplicate Event Triggers section - now handled in form.templ
|
||||
<div class="mb-6">
|
||||
<label for="secret_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Secret Key (for signature verification)</label>
|
||||
if data.NotificationService.SecretKey != "" {
|
||||
<input type="text" id="secret_key" name="secret_key" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Optional signature verification key" value={ data.NotificationService.SecretKey }/>
|
||||
}
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">If provided, all webhooks will include an X-GoMFT-Signature header</p>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="retry_policy" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Retry Policy</label>
|
||||
<select id="retry_policy" name="retry_policy" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||
if data.NotificationService.RetryPolicy != "" {
|
||||
if data.NotificationService.RetryPolicy == "none" {
|
||||
<option value="none" selected="selected">No retries</option>
|
||||
} else {
|
||||
<option value="none">No retries</option>
|
||||
}
|
||||
if data.NotificationService.RetryPolicy == "simple" {
|
||||
<option value="simple" selected="selected">Simple (3 retries)</option>
|
||||
} else {
|
||||
<option value="simple">Simple (3 retries)</option>
|
||||
}
|
||||
if data.NotificationService.RetryPolicy == "exponential" {
|
||||
<option value="exponential" selected="selected">Exponential backoff</option>
|
||||
} else {
|
||||
<option value="exponential">Exponential backoff</option>
|
||||
}
|
||||
} else {
|
||||
<option value="none">No retries</option>
|
||||
<option value="simple">Simple (3 retries)</option>
|
||||
<option value="exponential">Exponential backoff</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<!-- Test notification button -->
|
||||
<div class="mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h4 class="text-base font-medium text-gray-900 dark:text-white">Test Configuration</h4>
|
||||
<button
|
||||
type="button"
|
||||
id="test-webhook-btn"
|
||||
hx-post="/admin/settings/notifications/test"
|
||||
hx-trigger="click"
|
||||
hx-target="#test-notification-result"
|
||||
hx-swap="outerHTML"
|
||||
class="px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
||||
>
|
||||
<i class="fas fa-paper-plane mr-1"></i>
|
||||
Send Test Notification
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Send a test notification to verify your configuration works correctly before saving.
|
||||
</p>
|
||||
<div id="test-notification-result" class="mt-3 hidden">
|
||||
<!-- Result will be shown here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package fields
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||
// "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
|
||||
)
|
||||
|
||||
func WebhookFields(data types.NotificationFormData) 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"webhook_fields\" class=\"hidden notification-fields\"><div class=\"mb-6\"><label for=\"webhook_url\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Webhook URL</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.WebhookURL != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<input type=\"url\" id=\"webhook_url\" name=\"webhook_url\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"https://api.example.com/webhook\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.WebhookURL)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/webhook.templ`, Line: 13, Col: 415}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<input type=\"url\" id=\"webhook_url\" name=\"webhook_url\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"https://api.example.com/webhook\" value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div><div class=\"mb-6\"><label for=\"method\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">HTTP Method</label> <select id=\"method\" name=\"method\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.Method != "" {
|
||||
if data.NotificationService.Method == "POST" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<option value=\"POST\" selected=\"selected\">POST</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<option value=\"POST\">POST</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.Method == "PUT" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<option value=\"PUT\" selected=\"selected\">PUT</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<option value=\"PUT\">PUT</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<option value=\"POST\">POST</option> <option value=\"PUT\">PUT</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</select></div><div class=\"mb-6\"><label for=\"headers\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Headers (JSON)</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.Headers != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<textarea id=\"headers\" name=\"headers\" rows=\"3\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"{"Content-Type": "application/json", "Authorization": "Bearer token"}\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.Headers)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/webhook.templ`, Line: 41, Col: 437}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</textarea>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<textarea id=\"headers\" name=\"headers\" rows=\"3\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"{"Content-Type": "application/json", "Authorization": "Bearer token"}\"></textarea>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div><div class=\"mb-6\"><label for=\"payload_template\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Payload Template (JSON)</label> <textarea id=\"payload_template\" name=\"payload_template\" rows=\"5\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"{\n\t"event": "{{job.event}}",\n\t"job": {\n\t\t\t"id": "{{job.id}}",\n\t\t\t"name": "{{job.name}}",\n\t\t\t"status": "{{job.status}}",\n\t\t\t"message": "{{job.message}}",\n\t\t\t"started_at": "{{job.started_at}}",\n\t\t\t"completed_at": "{{job.completed_at}}",\n\t\t\t"duration_seconds": {{job.duration_seconds}},\n\t\t\t"config_id": "{{job.config_id}}",\n\t\t\t"config_name": "{{job.config_name}}",\n\t\t\t"transfer_bytes": {{job.transfer_bytes}},\n\t\t\t"file_count": {{job.file_count}}\n\t},\n\t"instance": {\n\t\t\t"id": "{{instance.id}}",\n\t\t\t"name": "{{instance.name}}",\n\t\t\t"version": "{{instance.version}}",\n\t\t\t"environment": "{{instance.environment}}"\n\t},\n\t"timestamp": "{{timestamp}}",\n\t"notification_id": "{{notification.id}}"\n}\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.PayloadTemplate != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "data.NotificationService.PayloadTemplate")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</textarea><p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p></div><div class=\"mb-6\"><label for=\"secret_key\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Secret Key (for signature verification)</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.SecretKey != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<input type=\"text\" id=\"secret_key\" name=\"secret_key\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"Optional signature verification key\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.SecretKey)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/webhook.templ`, Line: 87, Col: 417}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">If provided, all webhooks will include an X-GoMFT-Signature header</p></div><div class=\"mb-6\"><label for=\"retry_policy\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Retry Policy</label> <select id=\"retry_policy\" name=\"retry_policy\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.RetryPolicy != "" {
|
||||
if data.NotificationService.RetryPolicy == "none" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<option value=\"none\" selected=\"selected\">No retries</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<option value=\"none\">No retries</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, " ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.RetryPolicy == "simple" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<option value=\"simple\" selected=\"selected\">Simple (3 retries)</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<option value=\"simple\">Simple (3 retries)</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, " ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.RetryPolicy == "exponential" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<option value=\"exponential\" selected=\"selected\">Exponential backoff</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<option value=\"exponential\">Exponential backoff</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<option value=\"none\">No retries</option> <option value=\"simple\">Simple (3 retries)</option> <option value=\"exponential\">Exponential backoff</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</select></div><!-- Test notification button --><div class=\"mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600\"><div class=\"flex items-center justify-between mb-2\"><h4 class=\"text-base font-medium text-gray-900 dark:text-white\">Test Configuration</h4><button type=\"button\" id=\"test-webhook-btn\" hx-post=\"/admin/settings/notifications/test\" hx-trigger=\"click\" hx-target=\"#test-notification-result\" hx-swap=\"outerHTML\" class=\"px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-paper-plane mr-1\"></i> Send Test Notification</button></div><p class=\"text-sm text-gray-500 dark:text-gray-400\">Send a test notification to verify your configuration works correctly before saving.</p><div id=\"test-notification-result\" class=\"mt-3 hidden\"><!-- Result will be shown here --></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,177 @@
|
||||
package form
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/starfleetcptn/gomft/components" // For LayoutWithContext
|
||||
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||
"github.com/starfleetcptn/gomft/components/notifications/form/utils"
|
||||
"github.com/starfleetcptn/gomft/components/notifications/form/fields" // Import fields
|
||||
)
|
||||
|
||||
templ NotificationForm(ctx context.Context, data types.NotificationFormData) {
|
||||
@FormScripts() // Include the form-specific scripts
|
||||
@components.LayoutWithContext(utils.GetNotificationFormTitle(data.IsNew), ctx) {
|
||||
<!-- Status and Error Messages (Handled by shared toast component in layout) -->
|
||||
|
||||
<div id="notification-form-container" class="notifications-page bg-gray-50 dark:bg-gray-900 min-h-screen">
|
||||
<div class="pb-8 w-full max-w-4xl mx-auto">
|
||||
<!-- Success Message (hidden, used for HTMX responses) -->
|
||||
if data.SuccessMessage != "" {
|
||||
<div class="hidden success-message">{ data.SuccessMessage }</div>
|
||||
}
|
||||
<!-- Error Message (hidden, used for HTMX responses) -->
|
||||
if data.ErrorMessage != "" {
|
||||
<div class="hidden error-message">{ data.ErrorMessage }</div>
|
||||
}
|
||||
|
||||
<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">
|
||||
<i class="fas fa-bell w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i>
|
||||
{ utils.GetNotificationFormTitle(data.IsNew) }
|
||||
</h1>
|
||||
<a href="/admin/settings/notifications" class="flex items-center justify-center text-gray-700 bg-gray-100 hover:bg-gray-200 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg px-5 py-2.5 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 focus:outline-none dark:focus:ring-gray-700">
|
||||
<i class="fas fa-arrow-left w-4 h-4 mr-2"></i>
|
||||
Back to Notification Services
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Add Notification Service Form -->
|
||||
<div class="mb-6 p-6 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
||||
<form id="notification-form"
|
||||
if data.IsNew {
|
||||
hx-post="/admin/settings/notifications"
|
||||
} else {
|
||||
hx-put={ fmt.Sprintf("/admin/settings/notifications/%d", data.NotificationService.ID) }
|
||||
}
|
||||
hx-target="#notification-form-container">
|
||||
<div class="mb-6">
|
||||
<label for="notification_type" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Notification Type</label>
|
||||
<select id="notification_type" name="type" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||
<option value="">Select a type</option>
|
||||
if data.NotificationService != nil && data.NotificationService.Type == "webhook" {
|
||||
<option value="webhook" selected="selected">Webhook</option>
|
||||
} else {
|
||||
<option value="webhook">Webhook</option>
|
||||
}
|
||||
if data.NotificationService != nil && data.NotificationService.Type == "pushbullet" {
|
||||
<option value="pushbullet" selected="selected">Pushbullet</option>
|
||||
} else {
|
||||
<option value="pushbullet">Pushbullet</option>
|
||||
}
|
||||
if data.NotificationService != nil && data.NotificationService.Type == "ntfy" {
|
||||
<option value="ntfy" selected="selected">Ntfy</option>
|
||||
} else {
|
||||
<option value="ntfy">Ntfy</option>
|
||||
}
|
||||
if data.NotificationService != nil && data.NotificationService.Type == "gotify" {
|
||||
<option value="gotify" selected="selected">Gotify</option>
|
||||
} else {
|
||||
<option value="gotify">Gotify</option>
|
||||
}
|
||||
if data.NotificationService != nil && data.NotificationService.Type == "pushover" {
|
||||
<option value="pushover" selected="selected">Pushover</option>
|
||||
} else {
|
||||
<option value="pushover">Pushover</option>
|
||||
}
|
||||
<option value="email" disabled>Email (Coming Soon)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-6 hidden common-fields">
|
||||
<label for="notification_name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Name</label>
|
||||
if data.NotificationService.Name != "" {
|
||||
<input type="text" id="notification_name" name="name" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="My Notification Service" required value={ data.NotificationService.Name }/>
|
||||
} else {
|
||||
<input type="text" id="notification_name" name="name" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="My Notification Service" required value=""/>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-6 hidden common-fields">
|
||||
<label for="notification_description" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Description</label>
|
||||
if data.NotificationService.Description != "" {
|
||||
<textarea id="notification_description" name="description" rows="3" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Description for this notification service">{ data.NotificationService.Description }</textarea>
|
||||
} else {
|
||||
<textarea id="notification_description" name="description" rows="3" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Description for this notification service"></textarea>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Dynamic fields based on notification type -->
|
||||
@fields.EmailFields(data)
|
||||
@fields.WebhookFields(data)
|
||||
@fields.PushbulletFields(data)
|
||||
@fields.NtfyFields(data)
|
||||
@fields.GotifyFields(data)
|
||||
@fields.PushoverFields(data)
|
||||
|
||||
|
||||
<div class="mb-6 hidden common-fields">
|
||||
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Event Triggers</label>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-2">Select the job events that should trigger this notification.</p>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
for _, event := range []string{"job_start", "job_complete", "job_error"} {
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
id={ "trigger_" + event }
|
||||
name="event_triggers[]"
|
||||
type="checkbox"
|
||||
value={ event }
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
||||
if utils.IsEventTriggerSelected(data.NotificationService, event, data.IsNew) {
|
||||
checked
|
||||
}
|
||||
/>
|
||||
<label for={ "trigger_" + event } class="ml-2 text-sm font-medium text-gray-900 dark:text-gray-300">{ utils.FormatEventTriggerName(event) }</label>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start mb-6 hidden common-fields">
|
||||
<div class="flex items-center h-5">
|
||||
<input type="hidden" name="is_enabled" value="false">
|
||||
<input
|
||||
id="is_enabled"
|
||||
name="is_enabled"
|
||||
type="checkbox"
|
||||
value="true"
|
||||
class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"
|
||||
if data.NotificationService != nil && data.NotificationService.IsEnabled {
|
||||
checked
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-3 text-sm">
|
||||
<label for="is_enabled" class="font-medium text-gray-900 dark:text-white">Enable this notification service</label>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Check this box to make the service active.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden common-fields">
|
||||
<button type="submit" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||
if data.IsNew {
|
||||
Add Service
|
||||
} else {
|
||||
Save Changes
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Help Notice -->
|
||||
<div class="mt-8 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-800 dark:border-gray-700">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-info-circle text-blue-400 dark:text-blue-400"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-blue-700 dark:text-blue-400">
|
||||
Configure your notification service to receive alerts for job events. Different notification types have different configuration options.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Theme-specific background handled by Tailwind classes -->
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package form
|
||||
|
||||
// FormScripts contains JavaScript specific to the notification form page.
|
||||
templ FormScripts() {
|
||||
<script>
|
||||
// Toggle notification fields based on selection
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const typeSelector = document.getElementById('notification_type');
|
||||
// Ensure typeSelector exists before adding listener
|
||||
if (!typeSelector) {
|
||||
console.warn("Notification type selector not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const allFields = document.querySelectorAll('.notification-fields');
|
||||
const commonFields = document.querySelectorAll('.common-fields');
|
||||
|
||||
function toggleFields() {
|
||||
// Hide all specific fields first
|
||||
allFields.forEach(field => field.classList.add('hidden'));
|
||||
|
||||
// Show/hide common fields based on selection
|
||||
const selectedType = typeSelector.value;
|
||||
if (selectedType) {
|
||||
// Show common fields (name, description, is_enabled, submit)
|
||||
commonFields.forEach(field => field.classList.remove('hidden'));
|
||||
|
||||
// Show the selected type's specific fields
|
||||
const fieldsToShow = document.getElementById(`${selectedType}_fields`);
|
||||
if (fieldsToShow) {
|
||||
fieldsToShow.classList.remove('hidden');
|
||||
}
|
||||
} else {
|
||||
// Hide common fields if no type selected
|
||||
commonFields.forEach(field => field.classList.add('hidden'));
|
||||
}
|
||||
}
|
||||
|
||||
typeSelector.addEventListener('change', toggleFields);
|
||||
|
||||
// Initialize form state on load (if editing or if a type is pre-selected)
|
||||
toggleFields();
|
||||
});
|
||||
</script>
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package form
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
// FormScripts contains JavaScript specific to the notification form page.
|
||||
func FormScripts() 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<script>\n\t\t// Toggle notification fields based on selection\n\t\tdocument.addEventListener('DOMContentLoaded', function() {\n\t\t\tconst typeSelector = document.getElementById('notification_type');\n\t\t\t// Ensure typeSelector exists before adding listener\n\t\t\tif (!typeSelector) {\n\t\t\t\tconsole.warn(\"Notification type selector not found.\");\n\t\t\t\treturn; \n\t\t\t}\n\n\t\t\tconst allFields = document.querySelectorAll('.notification-fields');\n\t\t\tconst commonFields = document.querySelectorAll('.common-fields');\n\n\t\t\tfunction toggleFields() {\n\t\t\t\t// Hide all specific fields first\n\t\t\t\tallFields.forEach(field => field.classList.add('hidden'));\n\n\t\t\t\t// Show/hide common fields based on selection\n\t\t\t\tconst selectedType = typeSelector.value;\n\t\t\t\tif (selectedType) {\n\t\t\t\t\t// Show common fields (name, description, is_enabled, submit)\n\t\t\t\t\tcommonFields.forEach(field => field.classList.remove('hidden'));\n\n\t\t\t\t\t// Show the selected type's specific fields\n\t\t\t\t\tconst fieldsToShow = document.getElementById(`${selectedType}_fields`);\n\t\t\t\t\tif (fieldsToShow) {\n\t\t\t\t\t\tfieldsToShow.classList.remove('hidden');\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Hide common fields if no type selected\n\t\t\t\t\tcommonFields.forEach(field => field.classList.add('hidden'));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttypeSelector.addEventListener('change', toggleFields);\n\n\t\t\t// Initialize form state on load (if editing or if a type is pre-selected)\n\t\t\ttoggleFields(); \n\t\t});\n\t</script>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,398 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package form
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/starfleetcptn/gomft/components" // For LayoutWithContext
|
||||
"github.com/starfleetcptn/gomft/components/notifications/form/fields" // Import fields
|
||||
"github.com/starfleetcptn/gomft/components/notifications/form/utils"
|
||||
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||
)
|
||||
|
||||
func NotificationForm(ctx context.Context, data types.NotificationFormData) 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = FormScripts().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!-- Status and Error Messages (Handled by shared toast component in layout) --> <div id=\"notification-form-container\" class=\"notifications-page bg-gray-50 dark:bg-gray-900 min-h-screen\"><div class=\"pb-8 w-full max-w-4xl mx-auto\"><!-- Success Message (hidden, used for HTMX responses) -->")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.SuccessMessage != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"hidden success-message\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.SuccessMessage)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 21, Col: 62}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<!-- Error Message (hidden, used for HTMX responses) -->")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.ErrorMessage != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"hidden error-message\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.ErrorMessage)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 25, Col: 58}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<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\"><i class=\"fas fa-bell w-6 h-6 mr-2 text-blue-500 dark:text-blue-400\"></i> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(utils.GetNotificationFormTitle(data.IsNew))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 31, Col: 50}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</h1><a href=\"/admin/settings/notifications\" class=\"flex items-center justify-center text-gray-700 bg-gray-100 hover:bg-gray-200 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg px-5 py-2.5 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 focus:outline-none dark:focus:ring-gray-700\"><i class=\"fas fa-arrow-left w-4 h-4 mr-2\"></i> Back to Notification Services</a></div><!-- Add Notification Service Form --><div class=\"mb-6 p-6 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800\"><form id=\"notification-form\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.IsNew {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " hx-post=\"/admin/settings/notifications\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " hx-put=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("/admin/settings/notifications/%d", data.NotificationService.ID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 45, Col: 92}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " hx-target=\"#notification-form-container\"><div class=\"mb-6\"><label for=\"notification_type\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Notification Type</label> <select id=\"notification_type\" name=\"type\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"><option value=\"\">Select a type</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService != nil && data.NotificationService.Type == "webhook" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<option value=\"webhook\" selected=\"selected\">Webhook</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<option value=\"webhook\">Webhook</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if data.NotificationService != nil && data.NotificationService.Type == "pushbullet" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<option value=\"pushbullet\" selected=\"selected\">Pushbullet</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<option value=\"pushbullet\">Pushbullet</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if data.NotificationService != nil && data.NotificationService.Type == "ntfy" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<option value=\"ntfy\" selected=\"selected\">Ntfy</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<option value=\"ntfy\">Ntfy</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if data.NotificationService != nil && data.NotificationService.Type == "gotify" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<option value=\"gotify\" selected=\"selected\">Gotify</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<option value=\"gotify\">Gotify</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if data.NotificationService != nil && data.NotificationService.Type == "pushover" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<option value=\"pushover\" selected=\"selected\">Pushover</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<option value=\"pushover\">Pushover</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<option value=\"email\" disabled>Email (Coming Soon)</option></select></div><div class=\"mb-6 hidden common-fields\"><label for=\"notification_name\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Name</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.Name != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<input type=\"text\" id=\"notification_name\" name=\"name\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"My Notification Service\" required value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 83, Col: 414}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<input type=\"text\" id=\"notification_name\" name=\"name\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"My Notification Service\" required value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</div><div class=\"mb-6 hidden common-fields\"><label for=\"notification_description\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Description</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.Description != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<textarea id=\"notification_description\" name=\"description\" rows=\"3\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"Description for this notification service\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.Description)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 91, Col: 438}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</textarea>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<textarea id=\"notification_description\" name=\"description\" rows=\"3\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"Description for this notification service\"></textarea>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</div><!-- Dynamic fields based on notification type -->")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = fields.EmailFields(data).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = fields.WebhookFields(data).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = fields.PushbulletFields(data).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = fields.NtfyFields(data).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = fields.GotifyFields(data).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = fields.PushoverFields(data).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<div class=\"mb-6 hidden common-fields\"><label class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Event Triggers</label><p class=\"text-xs text-gray-500 dark:text-gray-400 mb-2\">Select the job events that should trigger this notification.</p><div class=\"flex flex-wrap gap-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, event := range []string{"job_start", "job_complete", "job_error"} {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<div class=\"flex items-center\"><input id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs("trigger_" + event)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 113, Col: 34}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\" name=\"event_triggers[]\" type=\"checkbox\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(event)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 116, Col: 24}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\" class=\"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if utils.IsEventTriggerSelected(data.NotificationService, event, data.IsNew) {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " checked")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "> <label for=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs("trigger_" + event)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 122, Col: 41}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\" class=\"ml-2 text-sm font-medium text-gray-900 dark:text-gray-300\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(utils.FormatEventTriggerName(event))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 122, Col: 147}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</label></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</div></div><div class=\"flex items-start mb-6 hidden common-fields\"><div class=\"flex items-center h-5\"><input id=\"is_enabled\" name=\"is_enabled\" type=\"checkbox\" value=\"true\" class=\"w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService != nil && data.NotificationService.IsEnabled {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, " checked")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "></div><div class=\"ml-3 text-sm\"><label for=\"is_enabled\" class=\"font-medium text-gray-900 dark:text-white\">Enable this notification service</label><p class=\"text-xs text-gray-500 dark:text-gray-400\">Check this box to make the service active.</p></div></div><div class=\"hidden common-fields\"><button type=\"submit\" class=\"text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.IsNew {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "Add Service")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "Save Changes")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</button></div></form></div><!-- Help Notice --><div class=\"mt-8 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-800 dark:border-gray-700\"><div class=\"flex\"><div class=\"flex-shrink-0\"><i class=\"fas fa-info-circle text-blue-400 dark:text-blue-400\"></i></div><div class=\"ml-3\"><p class=\"text-sm text-blue-700 dark:text-blue-400\">Configure your notification service to receive alerts for job events. Different notification types have different configuration options.</p></div></div></div></div></div><!-- Theme-specific background handled by Tailwind classes -->")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = components.LayoutWithContext(utils.GetNotificationFormTitle(data.IsNew), ctx).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,88 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetNotificationFormTitle returns the title for the notification form page.
|
||||
func GetNotificationFormTitle(isNew bool) string {
|
||||
if isNew {
|
||||
return "Add Notification Service"
|
||||
}
|
||||
return "Edit Notification Service"
|
||||
}
|
||||
|
||||
// Contains checks if a string slice contains a specific string.
|
||||
func Contains(slice []string, item string) bool {
|
||||
for _, a := range slice {
|
||||
if a == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// BoolToString converts a boolean to its string representation "true" or "false".
|
||||
// Useful for setting HTML attributes that expect string values.
|
||||
func BoolToString(b bool) string {
|
||||
if b {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
// IsEventTriggerSelected checks if a specific event trigger should be pre-selected.
|
||||
// It now accepts the anonymous struct type defined in types.NotificationFormData.
|
||||
// Defaults to checking 'job_complete' and 'job_error' when creating a new service.
|
||||
func IsEventTriggerSelected(service *struct {
|
||||
ID uint
|
||||
Name string
|
||||
Description string
|
||||
Type string
|
||||
IsEnabled bool
|
||||
EventTriggers []string
|
||||
RetryPolicy string
|
||||
WebhookURL string
|
||||
Method string
|
||||
Headers string
|
||||
PayloadTemplate string
|
||||
SecretKey string
|
||||
PushbulletAPIKey string
|
||||
PushbulletDeviceID string
|
||||
PushbulletTitleTemplate string
|
||||
PushbulletBodyTemplate string
|
||||
NtfyServer string
|
||||
NtfyTopic string
|
||||
NtfyPriority string
|
||||
NtfyUsername string
|
||||
NtfyPassword string
|
||||
NtfyTitleTemplate string
|
||||
NtfyMessageTemplate string
|
||||
GotifyURL string
|
||||
GotifyToken string
|
||||
GotifyPriority string
|
||||
GotifyTitleTemplate string
|
||||
GotifyMessageTemplate string
|
||||
PushoverAPIToken string
|
||||
PushoverUserKey string
|
||||
PushoverDevice string
|
||||
PushoverPriority string
|
||||
PushoverSound string
|
||||
PushoverTitleTemplate string
|
||||
PushoverMessageTemplate string
|
||||
}, event string, isNew bool) bool {
|
||||
if !isNew && service != nil {
|
||||
// Use the Contains helper function
|
||||
return Contains(service.EventTriggers, event)
|
||||
}
|
||||
// Default for new services: check complete and error
|
||||
return isNew && (event == "job_complete" || event == "job_error")
|
||||
}
|
||||
|
||||
// FormatEventTriggerName converts event trigger keys to human-readable names.
|
||||
func FormatEventTriggerName(event string) string {
|
||||
// Replace underscores with spaces and capitalize words
|
||||
name := strings.ReplaceAll(event, "_", " ")
|
||||
name = strings.Title(name) // Use strings.Title for capitalization
|
||||
return name
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package list
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/starfleetcptn/gomft/components"
|
||||
"github.com/starfleetcptn/gomft/components/notifications/dialog"
|
||||
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||
)
|
||||
|
||||
// List renders the notification services list page.
|
||||
templ List(ctx context.Context, data types.SettingsNotificationsData) {
|
||||
@components.LayoutWithContext("Notification Services", ctx) {
|
||||
<!-- Status and Error Messages (Handled by shared toast component in layout) -->
|
||||
|
||||
<div id="notifications-container" class="notifications-page bg-gray-50 dark:bg-gray-900 min-h-screen">
|
||||
<div class="pb-8 w-full">
|
||||
<!-- Success Message (hidden, used for HTMX responses/toast trigger) -->
|
||||
if data.SuccessMessage != "" {
|
||||
<div class="hidden success-message">{ data.SuccessMessage }</div>
|
||||
}
|
||||
<!-- Error Message (hidden, used for HTMX responses/toast trigger) -->
|
||||
if data.ErrorMessage != "" {
|
||||
<div class="hidden error-message">{ data.ErrorMessage }</div>
|
||||
}
|
||||
|
||||
<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">
|
||||
<i class="fas fa-bell w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i>
|
||||
Notification Services
|
||||
</h1>
|
||||
<a href="/admin/settings/notifications/new" class="flex items-center justify-center text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||
<i class="fas fa-plus w-4 h-4 mr-2"></i>
|
||||
Add Notification Service
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- List of Notification Services -->
|
||||
if len(data.NotificationServices) == 0 {
|
||||
<div class="text-center py-8 bg-white dark:bg-gray-800 shadow-md rounded-lg">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-blue-100 dark:bg-blue-900 mb-4">
|
||||
<i class="fas fa-bell text-2xl text-blue-600 dark:text-blue-400"></i>
|
||||
</div>
|
||||
<h3 class="mb-2 text-lg font-semibold text-gray-900 dark:text-white">No notification services configured</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400 mb-4">Add a notification service to receive alerts for job events.</p>
|
||||
<a href="/admin/settings/notifications/new" class="inline-flex items-center px-3 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg 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">
|
||||
<i class="fas fa-plus w-4 h-4 mr-2"></i>
|
||||
Add First Notification Service
|
||||
</a>
|
||||
</div>
|
||||
} else {
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 overflow-hidden">
|
||||
<ul class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
for _, service := range data.NotificationServices {
|
||||
<li>
|
||||
<div class="block hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
<div class="px-4 py-4 sm:px-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
if service.Type == "email" {
|
||||
<div class="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 dark:bg-blue-900 dark:text-blue-400 mr-3">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</div>
|
||||
} else if service.Type == "webhook" {
|
||||
<div class="w-10 h-10 rounded-full bg-green-100 flex items-center justify-center text-green-600 dark:bg-green-900 dark:text-green-400 mr-3">
|
||||
<i class="fas fa-code"></i>
|
||||
</div>
|
||||
} else { // Default icon
|
||||
<div class="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center text-gray-600 dark:bg-gray-700 dark:text-gray-400 mr-3">
|
||||
<i class="fas fa-bell"></i>
|
||||
</div>
|
||||
}
|
||||
<div>
|
||||
<p class="text-sm font-medium text-blue-600 dark:text-blue-400 truncate">
|
||||
{ service.Name }
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
{ service.Description }
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-2 flex-shrink-0 flex space-x-2">
|
||||
<a
|
||||
href={ templ.SafeURL(fmt.Sprintf("/admin/settings/notifications/%d/edit", service.ID)) }
|
||||
class="text-gray-500 bg-white focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 rounded-lg text-sm p-2 mr-1 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"
|
||||
>
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<!-- Add notification delete dialog -->
|
||||
@dialog.NotificationDialog(
|
||||
fmt.Sprintf("delete-notification-dialog-%d", service.ID),
|
||||
"Delete Notification Service",
|
||||
fmt.Sprintf("Are you sure you want to delete the notification service '%s'? This cannot be undone.", service.Name),
|
||||
"text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800",
|
||||
"Delete",
|
||||
"delete",
|
||||
service.ID,
|
||||
service.Name,
|
||||
)
|
||||
<button
|
||||
type="button"
|
||||
onclick={ templ.ComponentScript{Call: fmt.Sprintf("showModal('delete-notification-dialog-%d')", service.ID)} }
|
||||
class="text-red-500 bg-white focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 rounded-lg text-sm p-2 dark:bg-gray-800 dark:text-red-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"
|
||||
>
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 sm:flex sm:justify-between">
|
||||
<div class="sm:flex flex-col md:flex-row gap-2 md:gap-6">
|
||||
<div class="flex items-center">
|
||||
<span
|
||||
class={ "px-2 py-1 text-xs font-medium rounded-full",
|
||||
templ.KV("bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300", service.IsEnabled),
|
||||
templ.KV("bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300", !service.IsEnabled) }
|
||||
>
|
||||
if service.IsEnabled {
|
||||
Active
|
||||
} else {
|
||||
Disabled
|
||||
}
|
||||
</span>
|
||||
<span class="ml-2 px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300 rounded-full">
|
||||
{ service.Type }
|
||||
</span>
|
||||
if len(service.EventTriggers) > 0 && service.Type == "webhook" {
|
||||
<span class="ml-2 px-2 py-1 text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300 rounded-full">
|
||||
{ fmt.Sprintf("%d triggers", len(service.EventTriggers)) }
|
||||
</span>
|
||||
}
|
||||
if service.SuccessCount > 0 || service.FailureCount > 0 {
|
||||
<span class="ml-2 px-2 py-1 text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300 rounded-full">
|
||||
{ fmt.Sprintf("%d/%d", service.SuccessCount, service.SuccessCount + service.FailureCount) }
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
if service.Type == "webhook" {
|
||||
<div class="mt-2 md:mt-0 flex items-center space-x-4">
|
||||
<div class="text-xs">
|
||||
<span class="text-gray-500 dark:text-gray-400">Events:</span>
|
||||
<span class="ml-1 text-gray-900 dark:text-gray-300">
|
||||
if len(service.EventTriggers) == 0 {
|
||||
None
|
||||
} else {
|
||||
for i, trigger := range service.EventTriggers {
|
||||
if i > 0 {
|
||||
<span>, </span>
|
||||
}
|
||||
{ trigger }
|
||||
}
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs">
|
||||
<span class="text-gray-500 dark:text-gray-400">Retry:</span>
|
||||
<span class="ml-1 text-gray-900 dark:text-gray-300">
|
||||
if service.RetryPolicy == "" {
|
||||
Default
|
||||
} else {
|
||||
{ service.RetryPolicy }
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
} else {
|
||||
<div class="mt-2 md:mt-0 flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||
<i class="far fa-clock w-4 h-4 mr-1.5 text-gray-400 dark:text-gray-500"></i>
|
||||
<p>
|
||||
Last sent:
|
||||
if service.SuccessCount > 0 {
|
||||
"Recently"
|
||||
} else {
|
||||
"Never"
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Help Notice Placeholder -->
|
||||
<div class="mt-8 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-800 dark:border-gray-700">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-info-circle text-blue-400 dark:text-blue-400"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-blue-700 dark:text-blue-400">
|
||||
Notification services allow the system to send alerts for job events such as completion, errors, or when jobs start.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@dialog.DialogScripts()
|
||||
|
||||
}
|
||||
// Script call removed for now
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package list
|
||||
|
||||
// ListScripts contains JavaScript specific to the notification list page.
|
||||
templ ListScripts() {
|
||||
<script>
|
||||
// Notification system (showToast function is now in shared/toast/toast_js.templ)
|
||||
|
||||
// Track all HTMX events for debugging
|
||||
document.addEventListener('htmx:beforeRequest', function(event) {
|
||||
// Check if this is a DELETE request for a notification service
|
||||
const path = event.detail.path;
|
||||
const method = event.detail.verb;
|
||||
|
||||
console.log(`Request path: ${path}, method: ${method}`);
|
||||
|
||||
// Pattern match for notification service deletions (e.g., /admin/settings/notifications/123)
|
||||
if (path && method === 'DELETE' && path.match(/^\/admin\/settings\/notifications\/\d+$/)) {
|
||||
console.log("Detected notification service deletion request via URL pattern");
|
||||
|
||||
// This is definitely a delete request - store this information
|
||||
window.isServiceDeleteRequest = true;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('htmx:afterRequest', function(event) {
|
||||
// Check for notification service deletion multiple ways
|
||||
const isDeleteRequest =
|
||||
// Check global flag from the triggerServiceDelete function
|
||||
window.currentlyDeletingService ||
|
||||
// Check flag from beforeRequest handler
|
||||
window.isServiceDeleteRequest ||
|
||||
// Check URL pattern directly from this event
|
||||
(event.detail.pathInfo &&
|
||||
event.detail.pathInfo.requestPath &&
|
||||
event.detail.pathInfo.requestPath.match(/^\/admin\/settings\/notifications\/\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 serviceName = "Unknown";
|
||||
|
||||
// Try multiple sources for service name
|
||||
if (event.detail.elt && event.detail.elt.getAttribute) {
|
||||
serviceName = event.detail.elt.getAttribute('data-service-name') || serviceName;
|
||||
}
|
||||
|
||||
if (serviceName === "Unknown" && window.lastDeletedService) {
|
||||
// Fallback to our stored service info
|
||||
serviceName = window.lastDeletedService.name;
|
||||
}
|
||||
|
||||
console.log(`Showing success notification for deleted service: ${serviceName}`);
|
||||
// Ensure showToast is globally available
|
||||
if (typeof showToast === 'function') {
|
||||
showToast(`Notification service "${serviceName}" deleted successfully`, 'success');
|
||||
} else {
|
||||
console.error("showToast function not found!");
|
||||
}
|
||||
|
||||
|
||||
// Clear flags
|
||||
window.currentlyDeletingService = false;
|
||||
window.isServiceDeleteRequest = false;
|
||||
window.lastDeletedService = null;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('htmx:responseError', function(event) {
|
||||
console.log("HTMX response error:", event.detail);
|
||||
|
||||
// Similar logic as success but for errors
|
||||
const isDeleteRequest =
|
||||
window.currentlyDeletingService ||
|
||||
window.isServiceDeleteRequest ||
|
||||
(event.detail.pathInfo &&
|
||||
event.detail.pathInfo.requestPath &&
|
||||
event.detail.pathInfo.requestPath.match(/^\/admin\/settings\/notifications\/\d+$/) &&
|
||||
event.detail.verb === 'DELETE');
|
||||
|
||||
let errorMsg = 'An error occurred';
|
||||
if (event.detail.xhr && event.detail.xhr.responseText) {
|
||||
errorMsg = event.detail.xhr.responseText;
|
||||
}
|
||||
|
||||
if (isDeleteRequest) {
|
||||
console.log("Delete request failed");
|
||||
|
||||
let serviceName = "Unknown";
|
||||
|
||||
// Try multiple sources for service name
|
||||
if (event.detail.elt && event.detail.elt.getAttribute) {
|
||||
serviceName = event.detail.elt.getAttribute('data-service-name') || serviceName;
|
||||
}
|
||||
|
||||
if (serviceName === "Unknown" && window.lastDeletedService) {
|
||||
// Fallback to our stored service info
|
||||
serviceName = window.lastDeletedService.name;
|
||||
}
|
||||
|
||||
let specificErrorMsg = `Failed to delete notification service "${serviceName}"`;
|
||||
|
||||
if (event.detail.xhr && event.detail.xhr.responseText) {
|
||||
// Try to provide a more specific error from the response
|
||||
specificErrorMsg = `Error deleting "${serviceName}": ${event.detail.xhr.responseText}`;
|
||||
}
|
||||
|
||||
console.log(`Showing error notification: ${specificErrorMsg}`);
|
||||
if (typeof showToast === 'function') {
|
||||
showToast(specificErrorMsg, 'error');
|
||||
} else {
|
||||
console.error("showToast function not found!");
|
||||
}
|
||||
|
||||
|
||||
// Clear flags
|
||||
window.currentlyDeletingService = false;
|
||||
window.isServiceDeleteRequest = false;
|
||||
window.lastDeletedService = null;
|
||||
} else {
|
||||
// General error toast
|
||||
if (typeof showToast === 'function') {
|
||||
showToast(errorMsg, 'error');
|
||||
} else {
|
||||
console.error("showToast function not found!");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle modal hide buttons (This might be better placed globally or in layout if modals are used elsewhere)
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// This listener handles closing modals via data-modal-hide attribute
|
||||
// It might conflict or be redundant if Flowbite's JS handles this already.
|
||||
// Consider removing if Flowbite is initialized globally.
|
||||
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);
|
||||
if (modal) {
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
}
|
||||
const backdrop = document.getElementById(modalId + "-backdrop");
|
||||
if (backdrop) {
|
||||
backdrop.classList.add("hidden");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Show any success or error messages passed via data struct as toasts
|
||||
const successDiv = document.querySelector('.success-message');
|
||||
if (successDiv) {
|
||||
const successMsg = successDiv.textContent.trim();
|
||||
if (successMsg && typeof showToast === 'function') {
|
||||
showToast(successMsg, 'success');
|
||||
} else if (successMsg) {
|
||||
console.error("showToast function not found, cannot display success message:", successMsg);
|
||||
}
|
||||
}
|
||||
|
||||
const errorDiv = document.querySelector('.error-message');
|
||||
if (errorDiv) {
|
||||
const errorMsg = errorDiv.textContent.trim();
|
||||
if (errorMsg && typeof showToast === 'function') {
|
||||
showToast(errorMsg, 'error');
|
||||
} else if (errorMsg) {
|
||||
console.error("showToast function not found, cannot display error message:", errorMsg);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Script for handling the service deletion trigger
|
||||
function triggerServiceDelete(dialogId, serviceID, serviceName) {
|
||||
// Hide the dialog first
|
||||
closeModal(dialogId); // Reuse closeModal logic
|
||||
|
||||
// Add debugging info
|
||||
console.log(`Notification service deletion triggered for: ${serviceName} (ID: ${serviceID})`);
|
||||
|
||||
// Store data in a way that's accessible to event handlers
|
||||
window.lastDeletedService = {
|
||||
id: serviceID,
|
||||
name: serviceName
|
||||
};
|
||||
|
||||
// Add custom marker to track this deletion
|
||||
window.currentlyDeletingService = true;
|
||||
}
|
||||
|
||||
// Script for closing the modal
|
||||
function closeModal(id) {
|
||||
const dialog = document.getElementById(id);
|
||||
if (dialog) {
|
||||
dialog.classList.add("hidden");
|
||||
dialog.classList.remove("flex");
|
||||
}
|
||||
const backdrop = document.getElementById(id + "-backdrop");
|
||||
if (backdrop) {
|
||||
// Instead of removing, hide it to potentially reuse
|
||||
backdrop.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
// Script for showing the modal
|
||||
function showModal(id) {
|
||||
const dialog = document.getElementById(id);
|
||||
if (dialog) {
|
||||
dialog.classList.remove("hidden");
|
||||
dialog.classList.add("flex"); // Use flex to center content
|
||||
}
|
||||
const backdrop = document.getElementById(id + "-backdrop");
|
||||
if (backdrop) {
|
||||
backdrop.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,403 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package list
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/starfleetcptn/gomft/components"
|
||||
"github.com/starfleetcptn/gomft/components/notifications/dialog"
|
||||
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||
)
|
||||
|
||||
// List renders the notification services list page.
|
||||
func List(ctx context.Context, data types.SettingsNotificationsData) 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!-- Status and Error Messages (Handled by shared toast component in layout) --> <div id=\"notifications-container\" class=\"notifications-page bg-gray-50 dark:bg-gray-900 min-h-screen\"><div class=\"pb-8 w-full\"><!-- Success Message (hidden, used for HTMX responses/toast trigger) -->")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.SuccessMessage != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"hidden success-message\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.SuccessMessage)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/list/list.templ`, Line: 20, Col: 62}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<!-- Error Message (hidden, used for HTMX responses/toast trigger) -->")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.ErrorMessage != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"hidden error-message\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.ErrorMessage)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/list/list.templ`, Line: 24, Col: 58}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<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\"><i class=\"fas fa-bell w-6 h-6 mr-2 text-blue-500 dark:text-blue-400\"></i> Notification Services</h1><a href=\"/admin/settings/notifications/new\" class=\"flex items-center justify-center text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-plus w-4 h-4 mr-2\"></i> Add Notification Service</a></div><!-- List of Notification Services -->")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(data.NotificationServices) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"text-center py-8 bg-white dark:bg-gray-800 shadow-md rounded-lg\"><div class=\"inline-flex items-center justify-center w-16 h-16 rounded-full bg-blue-100 dark:bg-blue-900 mb-4\"><i class=\"fas fa-bell text-2xl text-blue-600 dark:text-blue-400\"></i></div><h3 class=\"mb-2 text-lg font-semibold text-gray-900 dark:text-white\">No notification services configured</h3><p class=\"text-gray-500 dark:text-gray-400 mb-4\">Add a notification service to receive alerts for job events.</p><a href=\"/admin/settings/notifications/new\" class=\"inline-flex items-center px-3 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg 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\"><i class=\"fas fa-plus w-4 h-4 mr-2\"></i> Add First Notification Service</a></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<div class=\"bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 overflow-hidden\"><ul class=\"divide-y divide-gray-200 dark:divide-gray-700\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, service := range data.NotificationServices {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<li><div class=\"block hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors\"><div class=\"px-4 py-4 sm:px-6\"><div class=\"flex items-center justify-between\"><div class=\"flex items-center\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if service.Type == "email" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 dark:bg-blue-900 dark:text-blue-400 mr-3\"><i class=\"fas fa-envelope\"></i></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else if service.Type == "webhook" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"w-10 h-10 rounded-full bg-green-100 flex items-center justify-center text-green-600 dark:bg-green-900 dark:text-green-400 mr-3\"><i class=\"fas fa-code\"></i></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " <div class=\"w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center text-gray-600 dark:bg-gray-700 dark:text-gray-400 mr-3\"><i class=\"fas fa-bell\"></i></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div><p class=\"text-sm font-medium text-blue-600 dark:text-blue-400 truncate\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(service.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/list/list.templ`, Line: 75, Col: 29}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</p><p class=\"text-sm text-gray-500 dark:text-gray-400 mt-1\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(service.Description)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/list/list.templ`, Line: 78, Col: 36}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</p></div></div><div class=\"ml-2 flex-shrink-0 flex space-x-2\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/admin/settings/notifications/%d/edit", service.ID))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var7)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" class=\"text-gray-500 bg-white focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 rounded-lg text-sm p-2 mr-1 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700\"><i class=\"fas fa-edit\"></i></a><!-- Add notification delete dialog -->")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = dialog.NotificationDialog(
|
||||
fmt.Sprintf("delete-notification-dialog-%d", service.ID),
|
||||
"Delete Notification Service",
|
||||
fmt.Sprintf("Are you sure you want to delete the notification service '%s'? This cannot be undone.", service.Name),
|
||||
"text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800",
|
||||
"Delete",
|
||||
"delete",
|
||||
service.ID,
|
||||
service.Name,
|
||||
).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("showModal('delete-notification-dialog-%d')", service.ID)})
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<button type=\"button\" onclick=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("showModal('delete-notification-dialog-%d')", service.ID)}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8.Call)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" class=\"text-red-500 bg-white focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 rounded-lg text-sm p-2 dark:bg-gray-800 dark:text-red-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700\"><i class=\"fas fa-trash-alt\"></i></button></div></div><div class=\"mt-3 sm:flex sm:justify-between\"><div class=\"sm:flex flex-col md:flex-row gap-2 md:gap-6\"><div class=\"flex items-center\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 = []any{"px-2 py-1 text-xs font-medium rounded-full",
|
||||
templ.KV("bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300", service.IsEnabled),
|
||||
templ.KV("bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300", !service.IsEnabled)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<span class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var9).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/list/list.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if service.IsEnabled {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "Active")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "Disabled")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</span> <span class=\"ml-2 px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300 rounded-full\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(service.Type)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/list/list.templ`, Line: 124, Col: 29}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(service.EventTriggers) > 0 && service.Type == "webhook" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<span class=\"ml-2 px-2 py-1 text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300 rounded-full\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d triggers", len(service.EventTriggers)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/list/list.templ`, Line: 128, Col: 72}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if service.SuccessCount > 0 || service.FailureCount > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<span class=\"ml-2 px-2 py-1 text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300 rounded-full\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d/%d", service.SuccessCount, service.SuccessCount+service.FailureCount))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/list/list.templ`, Line: 133, Col: 105}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if service.Type == "webhook" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<div class=\"mt-2 md:mt-0 flex items-center space-x-4\"><div class=\"text-xs\"><span class=\"text-gray-500 dark:text-gray-400\">Events:</span> <span class=\"ml-1 text-gray-900 dark:text-gray-300\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(service.EventTriggers) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "None")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
for i, trigger := range service.EventTriggers {
|
||||
if i > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<span>, </span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(trigger)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/list/list.templ`, Line: 150, Col: 27}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</span></div><div class=\"text-xs\"><span class=\"text-gray-500 dark:text-gray-400\">Retry:</span> <span class=\"ml-1 text-gray-900 dark:text-gray-300\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if service.RetryPolicy == "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "Default")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(service.RetryPolicy)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/list/list.templ`, Line: 161, Col: 38}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</span></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<div class=\"mt-2 md:mt-0 flex items-center text-sm text-gray-500 dark:text-gray-400\"><i class=\"far fa-clock w-4 h-4 mr-1.5 text-gray-400 dark:text-gray-500\"></i><p>Last sent: ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if service.SuccessCount > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\"Recently\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "\"Never\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</div></div></div></li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</ul></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<!-- Help Notice Placeholder --><div class=\"mt-8 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-800 dark:border-gray-700\"><div class=\"flex\"><div class=\"flex-shrink-0\"><i class=\"fas fa-info-circle text-blue-400 dark:text-blue-400\"></i></div><div class=\"ml-3\"><p class=\"text-sm text-blue-700 dark:text-blue-400\">Notification services allow the system to send alerts for job events such as completion, errors, or when jobs start.</p></div></div></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = dialog.DialogScripts().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = components.LayoutWithContext("Notification Services", ctx).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,69 @@
|
||||
package types
|
||||
|
||||
// SettingsNotificationsData defines the data needed for the notifications list page
|
||||
type SettingsNotificationsData struct {
|
||||
NotificationServices []NotificationServiceData
|
||||
SuccessMessage string
|
||||
ErrorMessage string
|
||||
}
|
||||
|
||||
// NotificationServiceData defines the data for a single service in the list
|
||||
type NotificationServiceData struct {
|
||||
ID uint
|
||||
Name string
|
||||
Type string
|
||||
IsEnabled bool
|
||||
Config map[string]string // Keep for now, might refine later
|
||||
Description string
|
||||
EventTriggers []string
|
||||
PayloadTemplate string
|
||||
SecretKey string
|
||||
RetryPolicy string
|
||||
SuccessCount int
|
||||
FailureCount int
|
||||
}
|
||||
|
||||
// NotificationFormData defines the data needed for the notification add/edit form
|
||||
// TODO: This will be moved from notification_form.templ later
|
||||
type NotificationFormData struct {
|
||||
NotificationService *struct {
|
||||
ID uint
|
||||
Name string
|
||||
Description string
|
||||
Type string
|
||||
IsEnabled bool
|
||||
EventTriggers []string
|
||||
RetryPolicy string
|
||||
WebhookURL string
|
||||
Method string
|
||||
Headers string
|
||||
PayloadTemplate string
|
||||
SecretKey string
|
||||
PushbulletAPIKey string
|
||||
PushbulletDeviceID string
|
||||
PushbulletTitleTemplate string
|
||||
PushbulletBodyTemplate string
|
||||
NtfyServer string
|
||||
NtfyTopic string
|
||||
NtfyPriority string
|
||||
NtfyUsername string
|
||||
NtfyPassword string
|
||||
NtfyTitleTemplate string
|
||||
NtfyMessageTemplate string
|
||||
GotifyURL string
|
||||
GotifyToken string
|
||||
GotifyPriority string
|
||||
GotifyTitleTemplate string
|
||||
GotifyMessageTemplate string
|
||||
PushoverAPIToken string
|
||||
PushoverUserKey string
|
||||
PushoverDevice string
|
||||
PushoverPriority string
|
||||
PushoverSound string
|
||||
PushoverTitleTemplate string
|
||||
PushoverMessageTemplate string
|
||||
}
|
||||
IsNew bool
|
||||
SuccessMessage string
|
||||
ErrorMessage string
|
||||
}
|
||||
@@ -138,21 +138,20 @@ templ ArchiveOptions() {
|
||||
</div>
|
||||
}
|
||||
|
||||
templ RcloneFlags() {
|
||||
templ RcloneFlags(currentCommandID uint) {
|
||||
<div class="mb-6">
|
||||
<label for="command_id" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Rclone Command</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-terminal text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
@RcloneCommandOptions()
|
||||
@RcloneCommandOptions(currentCommandID) // Pass it down
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Select the rclone command to use for this configuration.
|
||||
</p>
|
||||
|
||||
<!-- Command flags container - will be populated via HTMX -->
|
||||
<div id="command-flags-container" class="mt-4"></div>
|
||||
<!-- Flag container is now rendered directly in ConfigForm -->
|
||||
|
||||
<label for="rclone_flags" class="block mb-2 mt-6 text-sm font-medium text-gray-900 dark:text-white">Additional Rclone Flags</label>
|
||||
<div class="relative">
|
||||
@@ -171,12 +170,13 @@ templ RcloneFlags() {
|
||||
}
|
||||
|
||||
// New placeholder templ for rclone command options
|
||||
templ RcloneCommandOptions() {
|
||||
<div
|
||||
hx-get="/api/rclone/commands"
|
||||
templ RcloneCommandOptions(currentCommandID uint) { // Accept currentCommandID
|
||||
<div
|
||||
hx-get="/api/rclone/commands"
|
||||
hx-trigger="load"
|
||||
hx-target="this"
|
||||
hx-swap="outerHTML">
|
||||
hx-swap="outerHTML"
|
||||
hx-vals={ fmt.Sprintf(`{"commandId": %d}`, currentCommandID) }>
|
||||
<!-- Loading placeholder -->
|
||||
<option value="">Loading commands...</option>
|
||||
</div>
|
||||
@@ -231,12 +231,13 @@ templ DestinationSelection() {
|
||||
}
|
||||
|
||||
// RcloneCommandOptionsContent renders the command options organized by category
|
||||
templ RcloneCommandOptionsContent(categoryMap map[string][]db.RcloneCommand, categories []string) {
|
||||
templ RcloneCommandOptionsContent(categoryMap map[string][]db.RcloneCommand, categories []string, currentCommandID uint, commandFlagsJSON string, commandFlagValuesJSON string) { // Add flag JSON strings
|
||||
<select id="command_id" name="command_id" x-model="commandId"
|
||||
hx-get="/api/rclone/command-flags"
|
||||
hx-target="#command-flags-container"
|
||||
hx-trigger="change"
|
||||
hx-include="[name='command_id']"
|
||||
hx-vals={ fmt.Sprintf(`{"commandFlags": %s, "commandFlagValues": %s}`, commandFlagsJSON, commandFlagValuesJSON) }
|
||||
@change="updateCommandRequirements()"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||
<option value="">Select command...</option>
|
||||
@@ -244,7 +245,11 @@ templ RcloneCommandOptionsContent(categoryMap map[string][]db.RcloneCommand, cat
|
||||
if commands, ok := categoryMap[category]; ok && len(commands) > 0 {
|
||||
<optgroup label={ category }>
|
||||
for _, cmd := range commands {
|
||||
<option value={ fmt.Sprintf("%d", cmd.ID) }>{ cmd.Name } - { cmd.Description }</option>
|
||||
if cmd.ID == currentCommandID {
|
||||
<option value={ fmt.Sprintf("%d", cmd.ID) } selected>{ cmd.Name } - { cmd.Description }</option>
|
||||
} else {
|
||||
<option value={ fmt.Sprintf("%d", cmd.ID) }>{ cmd.Name } - { cmd.Description }</option>
|
||||
}
|
||||
}
|
||||
</optgroup>
|
||||
}
|
||||
@@ -253,7 +258,7 @@ templ RcloneCommandOptionsContent(categoryMap map[string][]db.RcloneCommand, cat
|
||||
}
|
||||
|
||||
// RcloneCommandFlagsContent renders the command flags for a selected command
|
||||
templ RcloneCommandFlagsContent(command *db.RcloneCommand) {
|
||||
templ RcloneCommandFlagsContent(command *db.RcloneCommand, selectedFlagsMap map[uint]bool, selectedFlagValues map[uint]string) {
|
||||
if command == nil {
|
||||
<div class="p-4 text-red-500">Command not found</div>
|
||||
return
|
||||
@@ -277,7 +282,9 @@ templ RcloneCommandFlagsContent(command *db.RcloneCommand) {
|
||||
name="command_flags"
|
||||
value={ fmt.Sprintf("%d", flag.ID) }
|
||||
class="mt-0.5 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:focus:ring-blue-600"
|
||||
/>
|
||||
if selectedFlagsMap[flag.ID] {
|
||||
checked
|
||||
} />
|
||||
<div class="ml-3">
|
||||
<label for={ fmt.Sprintf("flag_%d", flag.ID) } class="font-medium text-gray-900 dark:text-white">
|
||||
{ flag.Name } - { flag.Description }
|
||||
@@ -299,14 +306,16 @@ templ RcloneCommandFlagsContent(command *db.RcloneCommand) {
|
||||
class="mr-2 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:focus:ring-blue-600"
|
||||
data-input-id={ fmt.Sprintf("flag_value_%d", flag.ID) }
|
||||
onclick="toggleFlagValue(this)"
|
||||
/>
|
||||
if selectedFlagsMap[flag.ID] {
|
||||
checked
|
||||
} />
|
||||
<label for={ fmt.Sprintf("flag_enable_%d", flag.ID) } class="font-medium text-gray-900 dark:text-white">
|
||||
{ flag.Name } - { flag.Description }
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="w-full mt-2">
|
||||
@renderFlagInput(flag)
|
||||
@renderFlagInput(flag, selectedFlagValues[flag.ID], selectedFlagsMap[flag.ID]) // Pass value and enabled status
|
||||
if flag.DefaultValue != "" {
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Default: { flag.DefaultValue }</p>
|
||||
}
|
||||
@@ -345,23 +354,7 @@ templ RcloneCommandFlagsContent(command *db.RcloneCommand) {
|
||||
}
|
||||
|
||||
// Initialize all flag inputs on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const checkboxes = document.querySelectorAll('input[id^="flag_enable_"]');
|
||||
checkboxes.forEach(function(checkbox) {
|
||||
const inputId = checkbox.getAttribute('data-input-id');
|
||||
const input = document.getElementById(inputId);
|
||||
if (input) {
|
||||
input.disabled = !checkbox.checked;
|
||||
|
||||
// Also initialize the hidden input
|
||||
const hiddenId = inputId.replace('flag_value_', 'flag_hidden_');
|
||||
const hiddenInput = document.getElementById(hiddenId);
|
||||
if (hiddenInput) {
|
||||
hiddenInput.disabled = !checkbox.checked;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
// Initialization is now handled by server-side rendering
|
||||
</script>
|
||||
|
||||
<div class="mt-4 p-3 bg-blue-50 text-blue-800 rounded-lg border border-blue-100 dark:bg-blue-900/20 dark:text-blue-300 dark:border-blue-900 text-sm">
|
||||
@@ -402,7 +395,7 @@ templ RcloneCommandFlagsContent(command *db.RcloneCommand) {
|
||||
}
|
||||
|
||||
// Helper function to render appropriate input based on flag data type
|
||||
templ renderFlagInput(flag db.RcloneCommandFlag) {
|
||||
templ renderFlagInput(flag db.RcloneCommandFlag, value string, enabled bool) {
|
||||
if flag.DataType == "int" {
|
||||
<input
|
||||
type="number"
|
||||
@@ -410,14 +403,18 @@ templ renderFlagInput(flag db.RcloneCommandFlag) {
|
||||
name={ fmt.Sprintf("flag_value_%d", flag.ID) }
|
||||
class="w-full bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder={ flag.DefaultValue }
|
||||
disabled
|
||||
/>
|
||||
value={ value }
|
||||
if !enabled {
|
||||
disabled
|
||||
} />
|
||||
<!-- Hidden input to include this flag ID when checked -->
|
||||
<input
|
||||
type="hidden"
|
||||
name="command_flags"
|
||||
<input
|
||||
type="hidden"
|
||||
name="command_flags"
|
||||
value={ fmt.Sprintf("%d", flag.ID) }
|
||||
disabled
|
||||
if !enabled {
|
||||
disabled
|
||||
}
|
||||
id={ fmt.Sprintf("flag_hidden_%d", flag.ID) }
|
||||
data-enable-with={ fmt.Sprintf("flag_enable_%d", flag.ID) }
|
||||
/>
|
||||
@@ -429,14 +426,18 @@ templ renderFlagInput(flag db.RcloneCommandFlag) {
|
||||
step="0.01"
|
||||
class="w-full bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder={ flag.DefaultValue }
|
||||
disabled
|
||||
/>
|
||||
value={ value }
|
||||
if !enabled {
|
||||
disabled
|
||||
} />
|
||||
<!-- Hidden input to include this flag ID when checked -->
|
||||
<input
|
||||
type="hidden"
|
||||
name="command_flags"
|
||||
<input
|
||||
type="hidden"
|
||||
name="command_flags"
|
||||
value={ fmt.Sprintf("%d", flag.ID) }
|
||||
disabled
|
||||
if !enabled {
|
||||
disabled
|
||||
}
|
||||
id={ fmt.Sprintf("flag_hidden_%d", flag.ID) }
|
||||
data-enable-with={ fmt.Sprintf("flag_enable_%d", flag.ID) }
|
||||
/>
|
||||
@@ -448,14 +449,18 @@ templ renderFlagInput(flag db.RcloneCommandFlag) {
|
||||
name={ fmt.Sprintf("flag_value_%d", flag.ID) }
|
||||
class="w-full bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder={ flag.DefaultValue }
|
||||
disabled
|
||||
/>
|
||||
value={ value }
|
||||
if !enabled {
|
||||
disabled
|
||||
} />
|
||||
<!-- Hidden input to include this flag ID when checked -->
|
||||
<input
|
||||
type="hidden"
|
||||
name="command_flags"
|
||||
<input
|
||||
type="hidden"
|
||||
name="command_flags"
|
||||
value={ fmt.Sprintf("%d", flag.ID) }
|
||||
disabled
|
||||
if !enabled {
|
||||
disabled
|
||||
}
|
||||
id={ fmt.Sprintf("flag_hidden_%d", flag.ID) }
|
||||
data-enable-with={ fmt.Sprintf("flag_enable_%d", flag.ID) }
|
||||
/>
|
||||
|
||||
@@ -8,7 +8,7 @@ templ FTPDestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="dest_host" name="dest_host" x-model="destinationHost" x-bind:required="requiresDestination"
|
||||
<input type="text" id="dest_host" name="dest_host" x-model="destHost" x-bind:required="requiresDestination"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="ftp.example.com" />
|
||||
</div>
|
||||
@@ -21,9 +21,11 @@ templ FTPDestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-network-wired text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="number" id="dest_port" name="dest_port" x-model="destinationPort"
|
||||
<input type="number" id="dest_port" name="dest_port" x-model="destPort"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="21" min="1" max="65535" />
|
||||
placeholder="21" min="1" max="65535"
|
||||
x-init="if (!destPort || destPort === 0) destPort = 21"
|
||||
x-effect="if (destinationType === 'ftp' && (destPort === 0 || destPort === 22)) destPort = 21" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">FTP port number (default: 21)</p>
|
||||
</div>
|
||||
@@ -34,7 +36,7 @@ templ FTPDestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="dest_user" name="dest_user" x-model="destinationUser" x-bind:required="requiresDestination"
|
||||
<input type="text" id="dest_user" name="dest_user" x-model="destUser" x-bind:required="requiresDestination"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="username" />
|
||||
</div>
|
||||
@@ -47,7 +49,7 @@ templ FTPDestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="password" id="dest_password" name="dest_password" x-model="destinationPassword" x-bind:required="requiresDestination"
|
||||
<input type="password" id="dest_password" name="dest_password" x-model="destPassword" x-bind:required="requiresDestination"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="Enter password" />
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,7 @@ templ GoogleDriveDestinationForm() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div x-show="!useBuiltinAuthDestination">
|
||||
<div x-show="!useBuiltinAuthDest">
|
||||
<div class="p-4 mb-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 dark:bg-yellow-900/30 dark:text-yellow-300" role="alert">
|
||||
<div class="flex">
|
||||
<i class="fas fa-exclamation-triangle mr-2 flex-shrink-0"></i>
|
||||
@@ -37,9 +37,9 @@ templ GoogleDriveDestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-id-card text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="dest_client_id" name="dest_client_id" x-model="destinationClientId"
|
||||
<input type="text" id="dest_client_id" name="dest_client_id" x-model="destClientId"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="Your Google API Client ID" x-bind:required="!useBuiltinAuthDestination" />
|
||||
placeholder="Your Google API Client ID" x-bind:required="!useBuiltinAuthDest" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">OAuth 2.0 Client ID from Google Cloud Console</p>
|
||||
</div>
|
||||
@@ -50,9 +50,9 @@ templ GoogleDriveDestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="password" id="dest_client_secret" name="dest_client_secret" x-model="destinationClientSecret"
|
||||
<input type="password" id="dest_client_secret" name="dest_client_secret" x-model="destClientSecret"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="Your Google API Client Secret" x-bind:required="!useBuiltinAuthDestination" />
|
||||
placeholder="Your Google API Client Secret" x-bind:required="!useBuiltinAuthDest" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">OAuth 2.0 Client Secret from Google Cloud Console</p>
|
||||
</div>
|
||||
@@ -79,7 +79,7 @@ templ GoogleDriveDestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-fingerprint text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="dest_drive_id" name="dest_drive_id" x-model="destinationDriveId"
|
||||
<input type="text" id="dest_drive_id" name="dest_drive_id" x-model="destDriveId"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="Specific Google Drive ID" />
|
||||
</div>
|
||||
@@ -94,7 +94,7 @@ templ GoogleDriveDestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-users text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="dest_team_drive" name="dest_team_drive" x-model="destinationTeamDrive"
|
||||
<input type="text" id="dest_team_drive" name="dest_team_drive" x-model="destTeamDrive"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="Team Drive ID" />
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package destination
|
||||
|
||||
templ GooglePhotosDestinationForm() {
|
||||
<div class="space-y-6 mt-4" x-init="$watch('useBuiltinAuthDestination', value => {
|
||||
<div class="space-y-6 mt-4" x-init="$watch('useBuiltinAuthDest', value => {
|
||||
if(value) {
|
||||
destinationClientId = '';
|
||||
destinationClientSecret = '';
|
||||
destClientId = '';
|
||||
destClientSecret = '';
|
||||
}
|
||||
})">
|
||||
<div class="p-4 mb-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-blue-900/30 dark:text-blue-300" role="alert">
|
||||
@@ -38,7 +38,7 @@ templ GooglePhotosDestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-id-card text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="dest_client_id" name="dest_client_id" x-model="destinationClientId"
|
||||
<input type="text" id="dest_client_id" name="dest_client_id" x-model="destClientId"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
x-bind:disabled="useBuiltinAuthDest"
|
||||
placeholder="Google Photos OAuth Client ID" />
|
||||
@@ -57,7 +57,7 @@ templ GooglePhotosDestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="password" id="dest_client_secret" name="dest_client_secret" x-model="destinationClientSecret"
|
||||
<input type="password" id="dest_client_secret" name="dest_client_secret" x-model="destClientSecret"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
x-bind:disabled="useBuiltinAuthDest"
|
||||
placeholder="Google Photos OAuth Client Secret" />
|
||||
|
||||
@@ -15,7 +15,7 @@ templ MinIODestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="dest_endpoint" name="dest_endpoint" x-model="destinationEndpoint" x-bind:required="requiresDestination"
|
||||
<input type="text" id="dest_endpoint" name="dest_endpoint" x-model="destEndpoint" x-bind:required="requiresDestination"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="https://minio.example.com" />
|
||||
</div>
|
||||
@@ -30,7 +30,7 @@ templ MinIODestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-archive text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="dest_bucket" name="dest_bucket" x-model="destinationBucket" x-bind:required="requiresDestination"
|
||||
<input type="text" id="dest_bucket" name="dest_bucket" x-model="destBucket" x-bind:required="requiresDestination"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="my-bucket" />
|
||||
</div>
|
||||
@@ -39,13 +39,29 @@ templ MinIODestinationForm() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="dest_region" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Region (Optional)</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-globe-americas text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="dest_region" name="dest_region" x-model="destRegion"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="us-east-1" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Optional: Specify the region if your MinIO setup requires it.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="dest_access_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Access Key</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="dest_access_key" name="dest_access_key" x-model="destinationAccessKey" x-bind:required="requiresDestination"
|
||||
<input type="text" id="dest_access_key" name="dest_access_key" x-model="destAccessKey" x-bind:required="requiresDestination"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="minioadmin" />
|
||||
</div>
|
||||
@@ -60,7 +76,7 @@ templ MinIODestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="password" id="dest_secret_key" name="dest_secret_key" x-model="destinationSecretKey" x-bind:required="requiresDestination"
|
||||
<input type="password" id="dest_secret_key" name="dest_secret_key" x-model="destSecretKey" x-bind:required="requiresDestination"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="minioadmin" />
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ templ NextCloudDestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-cloud text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="dest_endpoint" name="dest_endpoint" x-model="destinationEndpoint" x-bind:required="requiresDestination"
|
||||
<input type="text" id="dest_endpoint" name="dest_endpoint" x-model="destEndpoint" x-bind:required="requiresDestination"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="https://nextcloud.example.com" />
|
||||
</div>
|
||||
@@ -30,7 +30,7 @@ templ NextCloudDestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="dest_user" name="dest_user" x-model="destinationUser" x-bind:required="requiresDestination"
|
||||
<input type="text" id="dest_user" name="dest_user" x-model="destUser" x-bind:required="requiresDestination"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="nextcloud_username" />
|
||||
</div>
|
||||
@@ -45,7 +45,7 @@ templ NextCloudDestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="password" id="dest_password" name="dest_password" x-model="destinationPassword" x-bind:required="requiresDestination"
|
||||
<input type="password" id="dest_password" name="dest_password" x-model="destPassword" x-bind:required="requiresDestination"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="NextCloud password" />
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ templ S3DestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-globe text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="dest_region" name="dest_region" x-model="destinationRegion" x-bind:required="requiresDestination"
|
||||
<input type="text" id="dest_region" name="dest_region" x-model="destRegion" x-bind:required="requiresDestination"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="us-east-1" />
|
||||
</div>
|
||||
@@ -30,7 +30,7 @@ templ S3DestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-archive text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="dest_bucket" name="dest_bucket" x-model="destinationBucket" x-bind:required="requiresDestination"
|
||||
<input type="text" id="dest_bucket" name="dest_bucket" x-model="destBucket" x-bind:required="requiresDestination"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="my-bucket" />
|
||||
</div>
|
||||
@@ -45,7 +45,7 @@ templ S3DestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="dest_access_key" name="dest_access_key" x-model="destinationAccessKey" x-bind:required="requiresDestination"
|
||||
<input type="text" id="dest_access_key" name="dest_access_key" x-model="destAccessKey" x-bind:required="requiresDestination"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="AKIAIOSFODNN7EXAMPLE" />
|
||||
</div>
|
||||
@@ -60,7 +60,7 @@ templ S3DestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="password" id="dest_secret_key" name="dest_secret_key" x-model="destinationSecretKey" x-bind:required="requiresDestination"
|
||||
<input type="password" id="dest_secret_key" name="dest_secret_key" x-model="destSecretKey" x-bind:required="requiresDestination"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" />
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ templ SFTPDestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="dest_host" name="dest_host" x-model="destinationHost" x-bind:required="requiresDestination"
|
||||
<input type="text" id="dest_host" name="dest_host" x-model="destHost" x-bind:required="requiresDestination"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="sftp.example.com" />
|
||||
</div>
|
||||
@@ -21,9 +21,9 @@ templ SFTPDestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-network-wired text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="number" id="dest_port" name="dest_port" x-model="destinationPort"
|
||||
<input type="number" id="dest_port" name="dest_port" x-model="destPort"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="22" min="1" max="65535" />
|
||||
placeholder="22" min="1" max="65535" x-init="if (!destPort || destPort === 0) destPort = 22" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">SFTP port number (default: 22)</p>
|
||||
</div>
|
||||
@@ -34,7 +34,7 @@ templ SFTPDestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="dest_user" name="dest_user" x-model="destinationUser" x-bind:required="requiresDestination"
|
||||
<input type="text" id="dest_user" name="dest_user" x-model="destUser" x-bind:required="requiresDestination"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="username" />
|
||||
</div>
|
||||
@@ -61,7 +61,7 @@ templ SFTPDestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="password" id="dest_password" name="dest_password" x-model="destinationPassword"
|
||||
<input type="password" id="dest_password" name="dest_password" x-model="destPassword"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="Enter password" x-bind:required="destAuthType === 'password'" />
|
||||
</div>
|
||||
@@ -74,7 +74,7 @@ templ SFTPDestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="dest_key_file" name="dest_key_file" x-model="destinationKeyFile"
|
||||
<input type="text" id="dest_key_file" name="dest_key_file" x-model="destKeyFile"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="/path/to/id_rsa" x-bind:required="destAuthType === 'key'" />
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ templ SMBDestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="dest_host" name="dest_host" x-model="destinationHost" x-bind:required="requiresDestination"
|
||||
<input type="text" id="dest_host" name="dest_host" x-model="destHost" x-bind:required="requiresDestination"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="192.168.1.100 or server.example.com" />
|
||||
</div>
|
||||
@@ -30,7 +30,7 @@ templ SMBDestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-share-alt text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="dest_share" name="dest_share" x-model="destinationShare" x-bind:required="requiresDestination"
|
||||
<input type="text" id="dest_share" name="dest_share" x-model="destShare" x-bind:required="requiresDestination"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="Documents" />
|
||||
</div>
|
||||
@@ -45,7 +45,7 @@ templ SMBDestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-sitemap text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="dest_domain" name="dest_domain" x-model="destinationDomain"
|
||||
<input type="text" id="dest_domain" name="dest_domain" x-model="destDomain"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="WORKGROUP or DOMAIN" />
|
||||
</div>
|
||||
@@ -60,7 +60,7 @@ templ SMBDestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="dest_user" name="dest_user" x-model="destinationUser" x-bind:required="requiresDestination"
|
||||
<input type="text" id="dest_user" name="dest_user" x-model="destUser" x-bind:required="requiresDestination"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="smb_username" />
|
||||
</div>
|
||||
@@ -75,7 +75,7 @@ templ SMBDestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="password" id="dest_password" name="dest_password" x-model="destinationPassword" x-bind:required="requiresDestination"
|
||||
<input type="password" id="dest_password" name="dest_password" x-model="destPassword" x-bind:required="requiresDestination"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="SMB password" />
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ templ WebDAVDestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-globe text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="dest_endpoint" name="dest_endpoint" x-model="destinationEndpoint" x-bind:required="requiresDestination"
|
||||
<input type="text" id="dest_endpoint" name="dest_endpoint" x-model="destEndpoint" x-bind:required="requiresDestination"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="https://webdav.example.com" />
|
||||
</div>
|
||||
@@ -30,7 +30,7 @@ templ WebDAVDestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="dest_user" name="dest_user" x-model="destinationUser" x-bind:required="requiresDestination"
|
||||
<input type="text" id="dest_user" name="dest_user" x-model="destUser" x-bind:required="requiresDestination"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="webdav_username" />
|
||||
</div>
|
||||
@@ -45,7 +45,7 @@ templ WebDAVDestinationForm() {
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="password" id="dest_password" name="dest_password" x-model="destinationPassword" x-bind:required="requiresDestination"
|
||||
<input type="password" id="dest_password" name="dest_password" x-model="destPassword" x-bind:required="requiresDestination"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="WebDAV password" />
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,7 @@ templ FTPSourceForm() {
|
||||
</div>
|
||||
<input type="number" id="source_port" name="source_port" x-model="sourcePort"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="21" min="1" max="65535" />
|
||||
placeholder="21" min="1" max="65535" value="21"/>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">FTP port number (default: 21)</p>
|
||||
</div>
|
||||
|
||||
@@ -39,6 +39,22 @@ templ MinIOSourceForm() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="source_region" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Region (Optional)</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-globe-americas text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="source_region" name="source_region" x-model="sourceRegion"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="us-east-1" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Optional: Specify the region if your MinIO setup requires it.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="source_access_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Access Key</label>
|
||||
<div class="relative">
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package toast
|
||||
|
||||
templ Container() {
|
||||
<div id="toast-container" class="fixed top-5 right-5 z-50 flex flex-col gap-2"></div>
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package toast
|
||||
|
||||
templ ShowToastJS() {
|
||||
<script>
|
||||
// Notification system
|
||||
function showToast(message, type) {
|
||||
const toastContainer = document.getElementById('toast-container');
|
||||
if (!toastContainer) {
|
||||
console.error("Toast container not found!");
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 { // Default to info
|
||||
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
|
||||
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);
|
||||
}
|
||||
</script>
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package toast
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
func ShowToastJS() 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<script>\n\t\t// Notification system\n\t\tfunction showToast(message, type) {\n\t\t\tconst toastContainer = document.getElementById('toast-container');\n\t\t\tif (!toastContainer) {\n\t\t\t\tconsole.error(\"Toast container not found!\");\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\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, bgColorClass, textColorClass;\n\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\tbgColorClass = 'text-green-500 dark:text-green-200';\n\t\t\t\ttextColorClass = 'text-green-500 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\tbgColorClass = 'text-red-500 dark:text-red-200';\n\t\t\t\ttextColorClass = 'text-red-500 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\tbgColorClass = 'text-blue-500 dark:text-blue-200';\n\t\t\t\ttextColorClass = 'text-blue-500 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\t// Animate out before removing\n\t\t\t\ttoast.classList.add('opacity-0', 'translate-y-4');\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\ttoast.remove();\n\t\t\t\t}, 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(() => {\n\t\t\t\t\ttoast.remove();\n\t\t\t\t}, 300);\n\t\t\t}, 5000);\n\t\t}\n\t</script>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,40 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package toast
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
func Container() 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"toast-container\" class=\"fixed top-5 right-5 z-50 flex flex-col gap-2\"></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,15 @@
|
||||
package components
|
||||
|
||||
templ TestResult(success bool, message string) {
|
||||
if success {
|
||||
<div class="text-green-600 dark:text-green-400 flex items-center">
|
||||
<i class="fas fa-check-circle mr-2"></i>
|
||||
<span>{ message }</span>
|
||||
</div>
|
||||
} else {
|
||||
<div class="text-red-600 dark:text-red-400 flex items-center">
|
||||
<i class="fas fa-exclamation-triangle mr-2"></i>
|
||||
<span>{ message }</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -117,9 +117,7 @@ templ UserManagementContent(data UserManagementData) {
|
||||
});
|
||||
|
||||
// Track user operations in HTMX events
|
||||
document.addEventListener('htmx:beforeRequest', function(event) {
|
||||
console.log("HTMX before request:", event.detail);
|
||||
|
||||
document.addEventListener('htmx:beforeRequest', function(event) {
|
||||
// Check if this is a DELETE request for a user
|
||||
const path = event.detail.path;
|
||||
const method = event.detail.verb;
|
||||
@@ -421,7 +419,7 @@ templ userList(users []db.User) {
|
||||
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"
|
||||
data-user-email={ user.Email }
|
||||
data-user-id={ strconv.Itoa(int(user.ID)) }
|
||||
onclick={ showUserDialog(fmt.Sprintf("delete-user-dialog-%d", user.ID)) }
|
||||
onclick={ showModal(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>
|
||||
@@ -634,7 +632,10 @@ func hasRole(user *db.User, roleID uint) bool {
|
||||
|
||||
// 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 id={ id } tabindex="-1" aria-hidden="true" class="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">
|
||||
<!-- Backdrop -->
|
||||
<div id={ fmt.Sprintf("%s-backdrop", id) } class="fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm"></div>
|
||||
<!-- Modal content -->
|
||||
<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">
|
||||
@@ -654,7 +655,7 @@ templ UserDialog(id string, title string, message string, confirmClass string, c
|
||||
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">
|
||||
<button type="button" onclick={ closeModal(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>
|
||||
@@ -663,16 +664,6 @@ templ UserDialog(id string, title string, message string, confirmClass string, c
|
||||
</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");
|
||||
|
||||
+43
-9
@@ -16,15 +16,49 @@ if [ -n "${PUID}" ] && [ -n "${PGID}" ]; then
|
||||
echo "Detected Alpine Linux, using busybox usermod/groupmod..."
|
||||
|
||||
# Update group ID first
|
||||
if [ "$(getent group ${USERNAME} | cut -d: -f3)" != "${PGID}" ]; then
|
||||
echo "Updating GID to ${PGID}..."
|
||||
groupmod -g ${PGID} ${USERNAME} || echo "⚠️ Failed to change GID"
|
||||
fi
|
||||
|
||||
# Update user ID
|
||||
if [ "$(id -u ${USERNAME})" != "${PUID}" ]; then
|
||||
echo "Updating UID to ${PUID}..."
|
||||
usermod -u ${PUID} ${USERNAME} || echo "⚠️ Failed to change UID"
|
||||
CURRENT_GID=$(getent group ${USERNAME} | cut -d: -f3)
|
||||
CURRENT_UID=$(id -u ${USERNAME})
|
||||
|
||||
if [ "${CURRENT_GID}" != "${PGID}" ] || [ "${CURRENT_UID}" != "${PUID}" ]; then
|
||||
echo "Attempting to update UID/GID to ${PUID}:${PGID} using delete/recreate..."
|
||||
|
||||
# Delete existing user and group, ignoring errors
|
||||
deluser ${USERNAME} > /dev/null 2>&1 || true
|
||||
delgroup ${USERNAME} > /dev/null 2>&1 || true
|
||||
|
||||
# Add group with the specified GID
|
||||
echo "Adding group ${USERNAME} with GID ${PGID}"
|
||||
if ! addgroup -g ${PGID} ${USERNAME}; then
|
||||
echo "⚠️ Failed to add group ${USERNAME} with GID ${PGID}."
|
||||
# Exiting because user creation will likely fail
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Add user with the specified UID and GID
|
||||
# Use -G for primary group with adduser in BusyBox
|
||||
# Use -h /app for home directory (consistent with expectations)
|
||||
# Use -s /bin/sh for shell
|
||||
# Use -D for no password (system user)
|
||||
echo "Adding user ${USERNAME} with UID ${PUID}"
|
||||
if ! adduser -u ${PUID} -G ${USERNAME} -h /app -s /bin/sh -D ${USERNAME}; then
|
||||
echo "⚠️ Failed to add user ${USERNAME} with UID ${PUID} and group ${USERNAME}."
|
||||
# Exiting because the application cannot run as the correct user
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify the change
|
||||
FINAL_UID=$(id -u ${USERNAME} 2>/dev/null || echo "error")
|
||||
FINAL_GID=$(getent group ${USERNAME} | cut -d: -f3 2>/dev/null || echo "error")
|
||||
|
||||
if [ "${FINAL_UID}" = "${PUID}" ] && [ "${FINAL_GID}" = "${PGID}" ]; then
|
||||
echo "✅ Successfully updated UID/GID to ${PUID}:${PGID}"
|
||||
else
|
||||
echo "⚠️ Verification failed after update. Target: ${PUID}:${PGID}, Actual: ${FINAL_UID}:${FINAL_GID}"
|
||||
# Exiting because the UID/GID is not correct
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "UID/GID ${PUID}:${PGID} already set."
|
||||
fi
|
||||
else
|
||||
echo "Non-Alpine system, using standard user management..."
|
||||
|
||||
@@ -12,6 +12,7 @@ require (
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
golang.org/x/crypto v0.36.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gorm.io/gorm v1.25.12
|
||||
@@ -22,6 +23,7 @@ require (
|
||||
github.com/bytedance/sonic v1.12.9 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.3 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/gin-contrib/sse v1.0.0 // indirect
|
||||
@@ -43,6 +45,7 @@ require (
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
|
||||
@@ -16,6 +16,7 @@ type Config struct {
|
||||
Email EmailConfig `json:"email"`
|
||||
BaseURL string `json:"base_url"` // Base URL for generating links in emails
|
||||
TOTPEncryptKey string `json:"totp_encrypt_key"` // Encryption key for TOTP secrets
|
||||
SkipSSLVerify bool `json:"skip_ssl_verify"` // Skip SSL verification for outgoing webhooks/notifications
|
||||
}
|
||||
|
||||
type EmailConfig struct {
|
||||
@@ -40,6 +41,7 @@ func Load() (*Config, error) {
|
||||
JWTSecret: "change_this_to_a_secure_random_string",
|
||||
BaseURL: "http://localhost:8080",
|
||||
TOTPEncryptKey: "this-is-a-dev-key-not-for-production!", // Default development key
|
||||
SkipSSLVerify: false, // Default to verifying SSL
|
||||
Email: EmailConfig{
|
||||
Enabled: false,
|
||||
Host: "smtp.example.com",
|
||||
@@ -119,6 +121,13 @@ func Load() (*Config, error) {
|
||||
if emailRequireAuth := os.Getenv("EMAIL_REQUIRE_AUTH"); emailRequireAuth != "" {
|
||||
cfg.Email.RequireAuth = strings.ToLower(emailRequireAuth) == "true"
|
||||
}
|
||||
|
||||
// Skip SSL Verification configuration
|
||||
if skipSSLVerify := os.Getenv("SKIP_SSL_VERIFY"); skipSSLVerify != "" {
|
||||
// If SKIP_SSL_VERIFY is set, parse its boolean value
|
||||
cfg.SkipSSLVerify = strings.ToLower(skipSSLVerify) == "true"
|
||||
}
|
||||
// Otherwise, the default from line 44 (false) is used.
|
||||
} else if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
} else {
|
||||
@@ -148,6 +157,11 @@ func Load() (*Config, error) {
|
||||
"EMAIL_REQUIRE_AUTH=" + strconv.FormatBool(cfg.Email.RequireAuth),
|
||||
"EMAIL_USERNAME=" + cfg.Email.Username,
|
||||
"EMAIL_PASSWORD=" + cfg.Email.Password,
|
||||
"",
|
||||
"# Skip SSL Verification for outgoing notifications (webhooks, etc.)",
|
||||
"# Set to true to disable SSL certificate verification (USE WITH CAUTION)",
|
||||
"# Defaults to false (verification enabled) if not set.",
|
||||
"SKIP_SSL_VERIFY=" + strconv.FormatBool(cfg.SkipSSLVerify), // Default is false
|
||||
}
|
||||
|
||||
if err := os.WriteFile(envPath, []byte(strings.Join(envContent, "\n")), 0644); err != nil {
|
||||
|
||||
@@ -28,7 +28,7 @@ type AuthProvider struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
Name string `gorm:"not null" json:"name"`
|
||||
Type ProviderType `gorm:"not null" json:"type"`
|
||||
Enabled bool `gorm:"default:true" json:"enabled"`
|
||||
Enabled *bool `gorm:"default:true" json:"enabled"`
|
||||
Description string `json:"description"`
|
||||
ProviderURL string `json:"provider_url"`
|
||||
ClientID string `json:"client_id"`
|
||||
@@ -75,6 +75,21 @@ func (p *AuthProvider) SetConfig(data map[string]interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- AuthProvider Helper Methods ---
|
||||
|
||||
// GetEnabled returns the value of Enabled with a default if nil
|
||||
func (p *AuthProvider) GetEnabled() bool {
|
||||
if p.Enabled == nil {
|
||||
return true // Default to true if not set
|
||||
}
|
||||
return *p.Enabled
|
||||
}
|
||||
|
||||
// SetEnabled sets the Enabled field
|
||||
func (p *AuthProvider) SetEnabled(value bool) {
|
||||
p.Enabled = &value
|
||||
}
|
||||
|
||||
// ExternalUserIdentity represents a user identity from an external authentication provider
|
||||
type ExternalUserIdentity struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
|
||||
-2045
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// FileMetadata stores information about processed files
|
||||
type FileMetadata struct {
|
||||
ID uint `gorm:"primarykey"`
|
||||
JobID uint `gorm:"not null;index"`
|
||||
Job Job `gorm:"foreignkey:JobID"`
|
||||
ConfigID uint `gorm:"default:0"` // The specific config ID this file was processed with
|
||||
FileName string `gorm:"not null"`
|
||||
OriginalPath string `gorm:"not null"`
|
||||
FileSize int64 `gorm:"not null"`
|
||||
FileHash string `gorm:"index"` // MD5 or other hash for file identity
|
||||
CreationTime time.Time
|
||||
ModTime time.Time
|
||||
ProcessedTime time.Time `gorm:"not null"`
|
||||
DestinationPath string `gorm:"not null"`
|
||||
Status string `gorm:"not null"` // processed, archived, deleted, etc.
|
||||
ErrorMessage string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package db
|
||||
|
||||
// --- FileMetadata Store Methods ---
|
||||
|
||||
// CreateFileMetadata creates a new file metadata record
|
||||
func (db *DB) CreateFileMetadata(metadata *FileMetadata) error {
|
||||
return db.Create(metadata).Error
|
||||
}
|
||||
|
||||
// GetFileMetadataByJobAndName retrieves file metadata by job ID and filename
|
||||
func (db *DB) GetFileMetadataByJobAndName(jobID uint, fileName string) (*FileMetadata, error) {
|
||||
var metadata FileMetadata
|
||||
err := db.Where("job_id = ? AND file_name = ?", jobID, fileName).First(&metadata).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &metadata, nil
|
||||
}
|
||||
|
||||
// GetFileMetadataByHash retrieves file metadata by file hash
|
||||
func (db *DB) GetFileMetadataByHash(fileHash string) (*FileMetadata, error) {
|
||||
var metadata FileMetadata
|
||||
err := db.Where("file_hash = ?", fileHash).First(&metadata).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &metadata, nil
|
||||
}
|
||||
|
||||
// DeleteFileMetadata deletes file metadata by ID
|
||||
func (db *DB) DeleteFileMetadata(id uint) error {
|
||||
return db.Delete(&FileMetadata{}, id).Error
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Job represents a scheduled transfer task
|
||||
type Job struct {
|
||||
ID uint `gorm:"primarykey"`
|
||||
Name string `form:"name"`
|
||||
ConfigID uint `gorm:"not null" form:"config_id"`
|
||||
Config TransferConfig `gorm:"foreignkey:ConfigID"`
|
||||
ConfigIDs string `gorm:"column:config_ids"` // Comma-separated list of config IDs
|
||||
Schedule string `gorm:"not null" form:"schedule"`
|
||||
Enabled *bool `gorm:"default:true" form:"enabled"`
|
||||
LastRun *time.Time
|
||||
NextRun *time.Time
|
||||
// Webhook notification fields
|
||||
WebhookEnabled *bool `gorm:"default:false" form:"webhook_enabled"`
|
||||
WebhookURL string `form:"webhook_url"`
|
||||
WebhookSecret string `form:"webhook_secret"`
|
||||
WebhookHeaders string `form:"webhook_headers"` // JSON-encoded headers
|
||||
NotifyOnSuccess *bool `gorm:"default:true" form:"notify_on_success"`
|
||||
NotifyOnFailure *bool `gorm:"default:true" form:"notify_on_failure"`
|
||||
CreatedBy uint
|
||||
User User `gorm:"foreignkey:CreatedBy"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// JobHistory records the execution history of a job
|
||||
type JobHistory struct {
|
||||
ID uint `gorm:"primarykey"`
|
||||
JobID uint `gorm:"not null"`
|
||||
Job Job `gorm:"foreignkey:JobID"`
|
||||
ConfigID uint `gorm:"default:0"` // The specific config ID this history entry is for
|
||||
StartTime time.Time `gorm:"not null"`
|
||||
EndTime *time.Time
|
||||
Status string `gorm:"not null"`
|
||||
BytesTransferred int64
|
||||
FilesTransferred int
|
||||
ErrorMessage string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// --- Job Helper Methods ---
|
||||
|
||||
// GetConfigIDsList returns the list of config IDs as integers
|
||||
func (j *Job) GetConfigIDsList() []uint {
|
||||
if j.ConfigIDs == "" {
|
||||
// If ConfigIDs is empty but ConfigID is set, return that as the only ID
|
||||
if j.ConfigID > 0 {
|
||||
return []uint{j.ConfigID}
|
||||
}
|
||||
return []uint{}
|
||||
}
|
||||
|
||||
// Split the comma-separated string
|
||||
strIDs := strings.Split(j.ConfigIDs, ",")
|
||||
ids := make([]uint, 0, len(strIDs))
|
||||
|
||||
// Convert each string to uint
|
||||
for _, strID := range strIDs {
|
||||
if id, err := strconv.ParseUint(strings.TrimSpace(strID), 10, 32); err == nil {
|
||||
ids = append(ids, uint(id))
|
||||
}
|
||||
}
|
||||
|
||||
return ids
|
||||
}
|
||||
|
||||
// SetConfigIDsList sets the config IDs from a slice of uint
|
||||
func (j *Job) SetConfigIDsList(ids []uint) {
|
||||
// Convert to strings
|
||||
strIDs := make([]string, len(ids))
|
||||
for i, id := range ids {
|
||||
strIDs[i] = strconv.FormatUint(uint64(id), 10)
|
||||
}
|
||||
|
||||
// Join with commas
|
||||
j.ConfigIDs = strings.Join(strIDs, ",")
|
||||
|
||||
// Debug log the final ConfigIDs string
|
||||
log.Printf("SetConfigIDsList: Setting ConfigIDs to: %s (from %v)", j.ConfigIDs, ids)
|
||||
|
||||
// If there's at least one ID, set ConfigID to the first one for backward compatibility
|
||||
if len(ids) > 0 {
|
||||
j.ConfigID = ids[0]
|
||||
} else {
|
||||
j.ConfigID = 0 // Ensure ConfigID is cleared if the list is empty
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfigIDsAsStrings returns the list of config IDs as strings for template rendering
|
||||
func (j *Job) GetConfigIDsAsStrings() []string {
|
||||
ids := j.GetConfigIDsList()
|
||||
strIDs := make([]string, len(ids))
|
||||
|
||||
for i, id := range ids {
|
||||
strIDs[i] = fmt.Sprintf("'%d'", id)
|
||||
}
|
||||
|
||||
return strIDs
|
||||
}
|
||||
|
||||
// GetEnabled returns the value of Enabled with a default if nil
|
||||
func (j *Job) GetEnabled() bool {
|
||||
if j.Enabled == nil {
|
||||
return true // Default to true if not set
|
||||
}
|
||||
return *j.Enabled
|
||||
}
|
||||
|
||||
// SetEnabled sets the Enabled field
|
||||
func (j *Job) SetEnabled(value bool) {
|
||||
j.Enabled = &value
|
||||
}
|
||||
|
||||
// GetWebhookEnabled returns the value of WebhookEnabled with a default if nil
|
||||
func (j *Job) GetWebhookEnabled() bool {
|
||||
if j.WebhookEnabled == nil {
|
||||
return false // Default to false if not set
|
||||
}
|
||||
return *j.WebhookEnabled
|
||||
}
|
||||
|
||||
// SetWebhookEnabled sets the WebhookEnabled field
|
||||
func (j *Job) SetWebhookEnabled(value bool) {
|
||||
j.WebhookEnabled = &value
|
||||
}
|
||||
|
||||
// GetNotifyOnSuccess returns the value of NotifyOnSuccess with a default if nil
|
||||
func (j *Job) GetNotifyOnSuccess() bool {
|
||||
if j.NotifyOnSuccess == nil {
|
||||
return true // Default to true if not set
|
||||
}
|
||||
return *j.NotifyOnSuccess
|
||||
}
|
||||
|
||||
// SetNotifyOnSuccess sets the NotifyOnSuccess field
|
||||
func (j *Job) SetNotifyOnSuccess(value bool) {
|
||||
j.NotifyOnSuccess = &value
|
||||
}
|
||||
|
||||
// GetNotifyOnFailure returns the value of NotifyOnFailure with a default if nil
|
||||
func (j *Job) GetNotifyOnFailure() bool {
|
||||
if j.NotifyOnFailure == nil {
|
||||
return true // Default to true if not set
|
||||
}
|
||||
return *j.NotifyOnFailure
|
||||
}
|
||||
|
||||
// SetNotifyOnFailure sets the NotifyOnFailure field
|
||||
func (j *Job) SetNotifyOnFailure(value bool) {
|
||||
j.NotifyOnFailure = &value
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
// --- Job Store Methods ---
|
||||
|
||||
// CreateJob creates a new job record
|
||||
func (db *DB) CreateJob(job *Job) error {
|
||||
// Use Omit to prevent GORM from creating a new config
|
||||
return db.Omit("Config").Create(job).Error
|
||||
}
|
||||
|
||||
// GetJobs retrieves all jobs for a user, preloading the associated config
|
||||
func (db *DB) GetJobs(userID uint) ([]Job, error) {
|
||||
var jobs []Job
|
||||
err := db.Preload("Config").Where("created_by = ?", userID).Find(&jobs).Error
|
||||
return jobs, err
|
||||
}
|
||||
|
||||
// GetJob retrieves a single job by ID, preloading the associated config
|
||||
func (db *DB) GetJob(id uint) (*Job, error) {
|
||||
var job Job
|
||||
err := db.Preload("Config").First(&job, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &job, nil
|
||||
}
|
||||
|
||||
// UpdateJob updates an existing job record
|
||||
func (db *DB) UpdateJob(job *Job) error {
|
||||
log.Printf("UpdateJob: Updating job ID: %d, ConfigIDs: %s", job.ID, job.ConfigIDs)
|
||||
|
||||
// Use Omit to prevent GORM from updating or creating a new config
|
||||
// Explicitly update fields that can be changed
|
||||
return db.Model(&Job{}).
|
||||
Where("id = ?", job.ID).
|
||||
Omit("Config"). // Omit the nested Config struct
|
||||
Updates(map[string]interface{}{
|
||||
"name": job.Name,
|
||||
"config_id": job.ConfigID, // Update the foreign key if needed
|
||||
"config_ids": job.ConfigIDs, // Explicitly update config_ids string
|
||||
"schedule": job.Schedule,
|
||||
"enabled": job.Enabled,
|
||||
"webhook_enabled": job.WebhookEnabled,
|
||||
"webhook_url": job.WebhookURL,
|
||||
"webhook_secret": job.WebhookSecret,
|
||||
"webhook_headers": job.WebhookHeaders,
|
||||
"notify_on_success": job.NotifyOnSuccess,
|
||||
"notify_on_failure": job.NotifyOnFailure,
|
||||
// Do not update LastRun, NextRun, CreatedBy, CreatedAt, UpdatedAt here
|
||||
// GORM handles UpdatedAt automatically
|
||||
}).Error
|
||||
}
|
||||
|
||||
// DeleteJob deletes a job and its associated history records
|
||||
func (db *DB) DeleteJob(id uint) error {
|
||||
// Start transaction
|
||||
tx := db.Begin()
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
// Delete associated job history records first
|
||||
if err := tx.Where("job_id = ?", id).Delete(&JobHistory{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("failed to delete job history: %v", err)
|
||||
}
|
||||
|
||||
// Delete the job
|
||||
if err := tx.Delete(&Job{}, id).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("failed to delete job: %v", err)
|
||||
}
|
||||
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
// UpdateJobStatus updates the LastRun and NextRun fields of a job
|
||||
func (db *DB) UpdateJobStatus(job *Job) error {
|
||||
// Only update specific fields related to run status
|
||||
return db.Model(job).Updates(map[string]interface{}{
|
||||
"last_run": job.LastRun,
|
||||
"next_run": job.NextRun,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// GetActiveJobs returns all active (enabled) jobs
|
||||
func (db *DB) GetActiveJobs() ([]Job, error) {
|
||||
if db.DB == nil {
|
||||
return nil, fmt.Errorf("database connection is nil")
|
||||
}
|
||||
var jobs []Job
|
||||
// For boolean pointer fields, need to check either NULL (for default) or true value
|
||||
err := db.Preload("Config").Where("enabled IS NULL OR enabled = ?", true).Find(&jobs).Error
|
||||
return jobs, err
|
||||
}
|
||||
|
||||
// GetConfigsForJob returns all transfer configurations associated with a job, in the order specified by ConfigIDs
|
||||
func (db *DB) GetConfigsForJob(jobID uint) ([]TransferConfig, error) {
|
||||
var job Job
|
||||
if err := db.First(&job, jobID).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get job %d: %w", jobID, err)
|
||||
}
|
||||
|
||||
configIDs := job.GetConfigIDsList()
|
||||
if len(configIDs) == 0 {
|
||||
return []TransferConfig{}, nil // No configs associated
|
||||
}
|
||||
|
||||
var configs []TransferConfig
|
||||
if err := db.Where("id IN ?", configIDs).Find(&configs).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get configs for job %d: %w", jobID, err)
|
||||
}
|
||||
|
||||
// Order the fetched configs according to the job.ConfigIDs list
|
||||
configMap := make(map[uint]TransferConfig, len(configs))
|
||||
for _, cfg := range configs {
|
||||
configMap[cfg.ID] = cfg
|
||||
}
|
||||
|
||||
orderedConfigs := make([]TransferConfig, 0, len(configIDs))
|
||||
for _, id := range configIDs {
|
||||
if cfg, ok := configMap[id]; ok {
|
||||
orderedConfigs = append(orderedConfigs, cfg)
|
||||
} else {
|
||||
log.Printf("Warning: Config ID %d listed in job %d not found in database", id, jobID)
|
||||
}
|
||||
}
|
||||
|
||||
return orderedConfigs, nil
|
||||
}
|
||||
|
||||
// --- JobHistory Store Methods ---
|
||||
|
||||
// CreateJobHistory creates a new job history record
|
||||
func (db *DB) CreateJobHistory(history *JobHistory) error {
|
||||
return db.Create(history).Error
|
||||
}
|
||||
|
||||
// UpdateJobHistory updates an existing job history record
|
||||
func (db *DB) UpdateJobHistory(history *JobHistory) error {
|
||||
return db.Save(history).Error
|
||||
}
|
||||
|
||||
// GetJobHistory retrieves all history records for a specific job, ordered by start time descending
|
||||
func (db *DB) GetJobHistory(jobID uint) ([]JobHistory, error) {
|
||||
var histories []JobHistory
|
||||
err := db.Where("job_id = ?", jobID).Order("start_time desc").Find(&histories).Error
|
||||
return histories, err
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// RecoverTransferConfigsRename checks for and corrects a specific inconsistent state
|
||||
// left by a potentially failed run of migration 012, where the transfer_configs
|
||||
// table might have been left renamed as _transfer_configs_old.
|
||||
func RecoverTransferConfigsRename() *gormigrate.Migration {
|
||||
return &gormigrate.Migration{
|
||||
ID: "011a_recover_transfer_configs_rename",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
fmt.Println("Running migration 011a: Checking for transfer_configs rename recovery...")
|
||||
|
||||
var oldTableExists int
|
||||
tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='_transfer_configs_old'").Scan(&oldTableExists)
|
||||
|
||||
var newTableExists int
|
||||
tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='transfer_configs'").Scan(&newTableExists)
|
||||
|
||||
if oldTableExists > 0 && newTableExists == 0 {
|
||||
fmt.Println("Found _transfer_configs_old table but not transfer_configs. Attempting recovery rename...")
|
||||
if err := tx.Exec("ALTER TABLE _transfer_configs_old RENAME TO transfer_configs").Error; err != nil {
|
||||
return fmt.Errorf("failed to rename _transfer_configs_old back to transfer_configs: %w", err)
|
||||
}
|
||||
fmt.Println("Successfully renamed _transfer_configs_old to transfer_configs.")
|
||||
} else if oldTableExists > 0 && newTableExists > 0 {
|
||||
// This state shouldn't ideally happen if migration 012 followed its logic,
|
||||
// but indicates a potential issue. Maybe drop the old one? For now, just log.
|
||||
fmt.Println("Warning: Both transfer_configs and _transfer_configs_old tables exist. Manual inspection might be needed.")
|
||||
} else {
|
||||
fmt.Println("No recovery needed for transfer_configs rename.")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
// Rollback doesn't make sense for a recovery step.
|
||||
fmt.Println("Rollback for migration 011a_recover_transfer_configs_rename is not applicable.")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AlterBooleanDefaults changes boolean columns with default:true to pointers
|
||||
// using explicit table recreation with raw SQL for SQLite compatibility.
|
||||
func AlterBooleanDefaults() *gormigrate.Migration {
|
||||
|
||||
// --- Raw SQL CREATE TABLE statements for the target schema ---
|
||||
|
||||
const createNotificationServicesSQL = `
|
||||
CREATE TABLE notification_services (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
is_enabled INTEGER DEFAULT 1, -- Target: *bool, SQLite uses 0/1, default true
|
||||
config TEXT,
|
||||
description TEXT,
|
||||
event_triggers TEXT DEFAULT '[]',
|
||||
payload_template TEXT,
|
||||
secret_key TEXT,
|
||||
retry_policy TEXT DEFAULT 'simple',
|
||||
last_used timestamp,
|
||||
success_count INTEGER DEFAULT 0,
|
||||
failure_count INTEGER DEFAULT 0,
|
||||
created_by INTEGER,
|
||||
created_at timestamp,
|
||||
updated_at timestamp
|
||||
);`
|
||||
|
||||
const createAuthProvidersSQL = `
|
||||
CREATE TABLE auth_providers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
enabled INTEGER DEFAULT 1, -- Target: *bool, SQLite uses 0/1, default true
|
||||
description TEXT,
|
||||
provider_url TEXT,
|
||||
client_id TEXT,
|
||||
client_secret TEXT,
|
||||
redirect_url TEXT,
|
||||
scopes TEXT,
|
||||
attribute_mapping TEXT,
|
||||
config TEXT,
|
||||
icon_url TEXT,
|
||||
successful_logins INTEGER DEFAULT 0,
|
||||
last_used timestamp,
|
||||
created_at timestamp,
|
||||
updated_at timestamp
|
||||
);`
|
||||
|
||||
const createTransferConfigsSQL = `
|
||||
CREATE TABLE transfer_configs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
source_type TEXT NOT NULL,
|
||||
source_path TEXT NOT NULL,
|
||||
source_host TEXT,
|
||||
source_port INTEGER DEFAULT 22,
|
||||
source_user TEXT,
|
||||
source_key_file TEXT,
|
||||
source_bucket TEXT,
|
||||
source_region TEXT,
|
||||
source_access_key TEXT,
|
||||
source_endpoint TEXT,
|
||||
source_share TEXT,
|
||||
source_domain TEXT,
|
||||
source_passive_mode INTEGER DEFAULT 1, -- Already *bool, keep default
|
||||
source_client_id TEXT,
|
||||
source_drive_id TEXT,
|
||||
source_team_drive TEXT,
|
||||
source_read_only INTEGER,
|
||||
source_start_year INTEGER,
|
||||
source_include_archived INTEGER,
|
||||
file_pattern TEXT DEFAULT '*',
|
||||
output_pattern TEXT,
|
||||
destination_type TEXT NOT NULL,
|
||||
destination_path TEXT NOT NULL,
|
||||
dest_host TEXT,
|
||||
dest_port INTEGER DEFAULT 22,
|
||||
dest_user TEXT,
|
||||
dest_key_file TEXT,
|
||||
dest_bucket TEXT,
|
||||
dest_region TEXT,
|
||||
dest_access_key TEXT,
|
||||
dest_endpoint TEXT,
|
||||
dest_share TEXT,
|
||||
dest_domain TEXT,
|
||||
dest_passive_mode INTEGER DEFAULT 1, -- Already *bool, keep default
|
||||
dest_client_id TEXT,
|
||||
dest_drive_id TEXT,
|
||||
dest_team_drive TEXT,
|
||||
dest_read_only INTEGER,
|
||||
dest_start_year INTEGER,
|
||||
dest_include_archived INTEGER,
|
||||
use_builtin_auth_source INTEGER,
|
||||
use_builtin_auth_dest INTEGER,
|
||||
google_drive_authenticated INTEGER,
|
||||
archive_path TEXT,
|
||||
archive_enabled INTEGER DEFAULT 0,
|
||||
rclone_flags TEXT,
|
||||
command_id INTEGER DEFAULT 1,
|
||||
command_flags TEXT,
|
||||
command_flag_values TEXT,
|
||||
delete_after_transfer INTEGER DEFAULT 0,
|
||||
skip_processed_files INTEGER DEFAULT 1, -- Target: *bool, SQLite uses 0/1, default true
|
||||
max_concurrent_transfers INTEGER DEFAULT 4,
|
||||
created_by INTEGER,
|
||||
created_at timestamp,
|
||||
updated_at timestamp
|
||||
);`
|
||||
|
||||
// --- End Raw SQL ---
|
||||
|
||||
return &gormigrate.Migration{
|
||||
ID: "012_alter_boolean_defaults",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
// --- Backup Logic (copied) ---
|
||||
var count int64
|
||||
if err := tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").Scan(&count).Error; err != nil {
|
||||
return fmt.Errorf("failed to check for existing tables: %v", err)
|
||||
}
|
||||
if count > 0 {
|
||||
sqlDB, err := tx.DB()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get underlying database: %v", err)
|
||||
}
|
||||
var seq int
|
||||
var name, dbPath string
|
||||
if err := sqlDB.QueryRow("PRAGMA database_list").Scan(&seq, &name, &dbPath); err != nil {
|
||||
return fmt.Errorf("failed to get database path: %v", err)
|
||||
}
|
||||
backupDir := os.Getenv("BACKUP_DIR")
|
||||
if backupDir == "" {
|
||||
backupDir = "/app/backups"
|
||||
if _, err := os.Stat(backupDir); os.IsNotExist(err) {
|
||||
backupDir = "backups"
|
||||
}
|
||||
}
|
||||
if err := os.MkdirAll(backupDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create backup directory: %v", err)
|
||||
}
|
||||
dbFileName := filepath.Base(dbPath)
|
||||
backupFileName := fmt.Sprintf("%s.backup.%s", dbFileName, time.Now().Format("20060102_150405"))
|
||||
backupFile := filepath.Join(backupDir, backupFileName)
|
||||
data, err := os.ReadFile(dbPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read database for backup: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(backupFile, data, 0600); err != nil {
|
||||
return fmt.Errorf("failed to create database backup: %v", err)
|
||||
}
|
||||
fmt.Printf("Created database backup at: %s\n", backupFile)
|
||||
}
|
||||
// --- End Backup Logic ---
|
||||
|
||||
// --- Table Recreation Logic for SQLite ---
|
||||
if err := tx.Exec("PRAGMA foreign_keys = OFF").Error; err != nil {
|
||||
return fmt.Errorf("failed to disable foreign keys: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := tx.Exec("PRAGMA foreign_keys = ON").Error; err != nil {
|
||||
fmt.Printf("Warning: failed to re-enable foreign keys: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Helper function for table recreation
|
||||
recreateTable := func(tableName, createSQL string) error {
|
||||
fmt.Printf("Recreating table %s...\n", tableName)
|
||||
oldTableName := fmt.Sprintf("_%s_old", tableName)
|
||||
|
||||
// Drop the old temp table if it exists from a previous failed run
|
||||
if err := tx.Exec(fmt.Sprintf("DROP TABLE IF EXISTS %s", oldTableName)).Error; err != nil {
|
||||
// Log the error but proceed, as the rename might still work or fail for the intended reason
|
||||
fmt.Printf("Warning: failed to drop potential leftover table %s: %v\n", oldTableName, err)
|
||||
}
|
||||
|
||||
// Rename old table
|
||||
if err := tx.Exec(fmt.Sprintf("ALTER TABLE %s RENAME TO %s", tableName, oldTableName)).Error; err == nil {
|
||||
fmt.Printf("Renamed %s to %s.\n", tableName, oldTableName)
|
||||
|
||||
// Create new table using raw SQL
|
||||
fmt.Printf("Creating new %s table...\n", tableName)
|
||||
if err := tx.Exec(createSQL).Error; err != nil {
|
||||
return fmt.Errorf("failed to create new %s table: %w", tableName, err)
|
||||
}
|
||||
fmt.Printf("New %s table created.\n", tableName)
|
||||
|
||||
// Copy data
|
||||
fmt.Printf("Copying data to new %s table...\n", tableName)
|
||||
// IMPORTANT: Ensure column order/names match if schema changed beyond types/defaults
|
||||
if err := tx.Exec(fmt.Sprintf("INSERT INTO %s SELECT * FROM %s", tableName, oldTableName)).Error; err != nil {
|
||||
return fmt.Errorf("failed to copy data to new %s table: %w", tableName, err)
|
||||
}
|
||||
fmt.Printf("Data copied to %s.\n", tableName)
|
||||
|
||||
// Drop old table
|
||||
if err := tx.Exec(fmt.Sprintf("DROP TABLE %s", oldTableName)).Error; err != nil {
|
||||
return fmt.Errorf("failed to drop old %s table: %w", tableName, err)
|
||||
}
|
||||
fmt.Printf("Successfully recreated %s.\n", tableName)
|
||||
} else {
|
||||
// Check if rename failed because table doesn't exist (fresh install)
|
||||
var tableExists int
|
||||
tx.Raw(fmt.Sprintf("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='%s'", tableName)).Scan(&tableExists)
|
||||
if tableExists == 0 {
|
||||
fmt.Printf("%s table does not exist, creating.\n", tableName)
|
||||
if err := tx.Exec(createSQL).Error; err != nil { // Create table directly
|
||||
return fmt.Errorf("failed to create new %s table: %w", tableName, err)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("failed to rename %s: %w", tableName, err) // Real rename error
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Recreate tables
|
||||
if err := recreateTable("notification_services", createNotificationServicesSQL); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := recreateTable("auth_providers", createAuthProvidersSQL); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := recreateTable("transfer_configs", createTransferConfigsSQL); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// --- End Table Recreation Logic ---
|
||||
|
||||
// Create audit log entry
|
||||
now := time.Now()
|
||||
details, auditErr := json.Marshal(map[string]interface{}{
|
||||
"tables_affected": []string{"notification_services", "auth_providers", "transfer_configs"},
|
||||
"columns_altered": []string{"is_enabled", "enabled", "skip_processed_files"},
|
||||
"new_type": "*bool (pointer to boolean)",
|
||||
"method": "Table recreation (SQLite - Raw SQL)",
|
||||
"message": "Changed boolean columns with default:true to pointers to handle false values correctly with GORM.",
|
||||
})
|
||||
if auditErr != nil {
|
||||
fmt.Printf("Warning: Failed to marshal audit log details: %v\n", auditErr)
|
||||
}
|
||||
|
||||
if auditErr == nil {
|
||||
if auditExecErr := tx.Exec(`
|
||||
INSERT INTO audit_logs (action, entity_type, entity_id, user_id, details, created_at, updated_at, timestamp)
|
||||
VALUES ('schema_update', 'multiple_tables', 0, 1, ?, ?, ?, ?)
|
||||
`, string(details), now, now, now).Error; auditExecErr != nil {
|
||||
fmt.Printf("Warning: Failed to insert audit log: %v\n", auditExecErr)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
// Rollback is complex and risky with table recreation. Log skip.
|
||||
now := time.Now()
|
||||
details, err := json.Marshal(map[string]interface{}{
|
||||
"migration_id": "012_alter_boolean_defaults",
|
||||
"message": "Skipping rollback of boolean column type changes (via table recreation) due to complexity/potential data loss.",
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: Failed to marshal rollback audit log details: %v\n", err)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
if auditExecErr := tx.Exec(`
|
||||
INSERT INTO audit_logs (action, entity_type, entity_id, user_id, details, created_at, updated_at, timestamp)
|
||||
VALUES ('migration_rollback', 'multiple_tables', 0, 1, ?, ?, ?, ?)
|
||||
`, string(details), now, now, now).Error; auditExecErr != nil {
|
||||
fmt.Printf("Warning: Failed to insert rollback audit log: %v\n", auditExecErr)
|
||||
}
|
||||
}
|
||||
fmt.Println("Rollback for migration 012_alter_boolean_defaults skipped for safety.")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// CleanupInvalidBooleans updates boolean columns represented as integers
|
||||
// to ensure they only contain valid values (0, 1, or NULL).
|
||||
func CleanupInvalidBooleans() *gormigrate.Migration {
|
||||
return &gormigrate.Migration{
|
||||
ID: "013_cleanup_invalid_booleans",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
fmt.Println("Running migration 013: Cleaning up invalid boolean values...")
|
||||
|
||||
// Target: transfer_configs.delete_after_transfer
|
||||
// Set any non-NULL value that is not 0 or 1 to 0 (false)
|
||||
sql := `UPDATE transfer_configs
|
||||
SET delete_after_transfer = 0
|
||||
WHERE delete_after_transfer IS NOT NULL AND delete_after_transfer NOT IN (0, 1);`
|
||||
|
||||
if err := tx.Exec(sql).Error; err != nil {
|
||||
return fmt.Errorf("failed to cleanup delete_after_transfer in transfer_configs: %w", err)
|
||||
}
|
||||
fmt.Println("Cleaned up invalid values in transfer_configs.delete_after_transfer.")
|
||||
|
||||
// Target: transfer_configs.archive_enabled
|
||||
sql = `UPDATE transfer_configs
|
||||
SET archive_enabled = 0
|
||||
WHERE archive_enabled IS NOT NULL AND archive_enabled NOT IN (0, 1);`
|
||||
if err := tx.Exec(sql).Error; err != nil {
|
||||
return fmt.Errorf("failed to cleanup archive_enabled in transfer_configs: %w", err)
|
||||
}
|
||||
fmt.Println("Cleaned up invalid values in transfer_configs.archive_enabled.")
|
||||
|
||||
// Target: transfer_configs.skip_processed_files
|
||||
sql = `UPDATE transfer_configs
|
||||
SET skip_processed_files = 0
|
||||
WHERE skip_processed_files IS NOT NULL AND skip_processed_files NOT IN (0, 1);`
|
||||
if err := tx.Exec(sql).Error; err != nil {
|
||||
return fmt.Errorf("failed to cleanup skip_processed_files in transfer_configs: %w", err)
|
||||
}
|
||||
fmt.Println("Cleaned up invalid values in transfer_configs.skip_processed_files.")
|
||||
|
||||
// Target: notification_services.is_enabled
|
||||
sql = `UPDATE notification_services
|
||||
SET is_enabled = 0
|
||||
WHERE is_enabled IS NOT NULL AND is_enabled NOT IN (0, 1);`
|
||||
if err := tx.Exec(sql).Error; err != nil {
|
||||
// Check if the table exists before failing hard
|
||||
var tableExists int
|
||||
tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='notification_services'").Scan(&tableExists)
|
||||
if tableExists == 0 {
|
||||
fmt.Println("Skipping cleanup for notification_services.is_enabled: table does not exist.")
|
||||
} else {
|
||||
return fmt.Errorf("failed to cleanup is_enabled in notification_services: %w", err)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("Cleaned up invalid values in notification_services.is_enabled.")
|
||||
}
|
||||
|
||||
// Target: auth_providers.enabled
|
||||
sql = `UPDATE auth_providers
|
||||
SET enabled = 0
|
||||
WHERE enabled IS NOT NULL AND enabled NOT IN (0, 1);`
|
||||
if err := tx.Exec(sql).Error; err != nil {
|
||||
// Check if the table exists before failing hard
|
||||
var tableExists int
|
||||
tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='auth_providers'").Scan(&tableExists)
|
||||
if tableExists == 0 {
|
||||
fmt.Println("Skipping cleanup for auth_providers.enabled: table does not exist.")
|
||||
} else {
|
||||
return fmt.Errorf("failed to cleanup enabled in auth_providers: %w", err)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("Cleaned up invalid values in auth_providers.enabled.")
|
||||
}
|
||||
|
||||
fmt.Println("Migration 013 completed successfully.")
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
// This migration cleans up data. Rolling back doesn't make sense
|
||||
// as we don't know the original invalid values.
|
||||
fmt.Println("Rollback for migration 013_cleanup_invalid_booleans is not applicable.")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -11,17 +11,20 @@ var migrations []*gormigrate.Migration
|
||||
func GetMigrations(db *gorm.DB) *gormigrate.Gormigrate {
|
||||
// Add all migrations in order
|
||||
migrations = append(migrations,
|
||||
InitialSchema(), // 001
|
||||
UpdateGDriveType(), // 002
|
||||
Add2FA(), // 003
|
||||
AddAuditLogs(), // 004
|
||||
AddDefaultRoles(), // 005
|
||||
AddTimestampsToJobHistories(), // 006
|
||||
AddNotificationServices(), // 007
|
||||
AddUserNotifications(), // 008
|
||||
AddRcloneTables(), // 009
|
||||
AddRcloneCommandToConfig(), // 010
|
||||
AddAuthProviders(), // 011
|
||||
InitialSchema(), // 001
|
||||
UpdateGDriveType(), // 002
|
||||
Add2FA(), // 003
|
||||
AddAuditLogs(), // 004
|
||||
AddDefaultRoles(), // 005
|
||||
AddTimestampsToJobHistories(), // 006
|
||||
AddNotificationServices(), // 007
|
||||
AddUserNotifications(), // 008
|
||||
AddRcloneTables(), // 009
|
||||
AddRcloneCommandToConfig(), // 010
|
||||
AddAuthProviders(), // 011
|
||||
RecoverTransferConfigsRename(), // 011a
|
||||
AlterBooleanDefaults(), // 012
|
||||
CleanupInvalidBooleans(), // 013
|
||||
)
|
||||
|
||||
return gormigrate.New(db, gormigrate.DefaultOptions, migrations)
|
||||
|
||||
@@ -12,7 +12,7 @@ type NotificationService struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
Name string `json:"name" gorm:"not null"`
|
||||
Type string `json:"type" gorm:"not null"` // email, webhook
|
||||
IsEnabled bool `json:"is_enabled" gorm:"default:true"`
|
||||
IsEnabled *bool `json:"is_enabled" gorm:"default:true"`
|
||||
Config map[string]string `json:"config" gorm:"-"`
|
||||
ConfigJSON string `json:"-" gorm:"column:config"`
|
||||
Description string `json:"description"`
|
||||
@@ -69,6 +69,8 @@ func (db *DB) GetNotificationServices(onlyEnabled bool) ([]NotificationService,
|
||||
query := db.DB
|
||||
|
||||
if onlyEnabled {
|
||||
// When using a pointer, we need to explicitly check for true
|
||||
// GORM handles the underlying SQL correctly for different dialects
|
||||
query = query.Where("is_enabled = ?", true)
|
||||
}
|
||||
|
||||
@@ -102,3 +104,21 @@ func (db *DB) UpdateNotificationService(service *NotificationService) error {
|
||||
func (db *DB) DeleteNotificationService(id uint) error {
|
||||
return db.Delete(&NotificationService{}, id).Error
|
||||
}
|
||||
|
||||
// --- NotificationService Helper Methods ---
|
||||
|
||||
// GetIsEnabled returns the value of IsEnabled with a default if nil
|
||||
func (n *NotificationService) GetIsEnabled() bool {
|
||||
if n.IsEnabled == nil {
|
||||
// If the pointer is nil, GORM might not have set it,
|
||||
// or it was explicitly set to nil. We assume the DB default (true)
|
||||
// if it's nil, aligning with the original gorm tag default.
|
||||
return true
|
||||
}
|
||||
return *n.IsEnabled
|
||||
}
|
||||
|
||||
// SetIsEnabled sets the IsEnabled field
|
||||
func (n *NotificationService) SetIsEnabled(value bool) {
|
||||
n.IsEnabled = &value
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RcloneCommand represents a command available in rclone
|
||||
type RcloneCommand struct {
|
||||
ID uint `gorm:"primarykey"`
|
||||
Name string `gorm:"not null;uniqueIndex"`
|
||||
Description string `gorm:"not null"`
|
||||
Category string `gorm:"not null;index"`
|
||||
IsAdvanced bool `gorm:"not null;default:false"`
|
||||
Flags []RcloneCommandFlag `gorm:"foreignKey:CommandID;constraint:OnDelete:CASCADE"`
|
||||
CreatedAt time.Time `gorm:"not null"`
|
||||
}
|
||||
|
||||
// RcloneCommandFlag represents a flag that can be used with an rclone command
|
||||
type RcloneCommandFlag struct {
|
||||
ID uint `gorm:"primarykey"`
|
||||
CommandID uint `gorm:"not null;index"`
|
||||
Command RcloneCommand `gorm:"foreignKey:CommandID"`
|
||||
Name string `gorm:"not null;index"`
|
||||
ShortName string
|
||||
Description string `gorm:"not null"`
|
||||
DataType string `gorm:"not null"` // string, int, bool, etc.
|
||||
IsRequired bool `gorm:"not null;default:false"`
|
||||
DefaultValue string
|
||||
CreatedAt time.Time `gorm:"not null"`
|
||||
}
|
||||
|
||||
// --- Rclone Helper Methods ---
|
||||
|
||||
// GetUsageExample returns a human-readable usage example for a flag
|
||||
func (flag *RcloneCommandFlag) GetUsageExample() string {
|
||||
switch flag.DataType {
|
||||
case "bool":
|
||||
return flag.Name
|
||||
case "int":
|
||||
return fmt.Sprintf("%s=<number>", flag.Name)
|
||||
case "float":
|
||||
return fmt.Sprintf("%s=<decimal>", flag.Name)
|
||||
case "string":
|
||||
return fmt.Sprintf("%s=<text>", flag.Name)
|
||||
default:
|
||||
return fmt.Sprintf("%s=<value>", flag.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// ParseRcloneFlags parses a string of rclone flags into a map
|
||||
// Note: This is a general utility function, not tied to a specific struct instance.
|
||||
// It might be better placed in a more general utility package if one exists,
|
||||
// but keeping it here for now as per the original file structure.
|
||||
func ParseRcloneFlags(flagsStr string) map[string]string {
|
||||
result := make(map[string]string)
|
||||
if flagsStr == "" {
|
||||
return result
|
||||
}
|
||||
|
||||
// Split the flags string by spaces
|
||||
parts := strings.Fields(flagsStr)
|
||||
|
||||
for i := 0; i < len(parts); i++ {
|
||||
part := parts[i]
|
||||
|
||||
// Check if it's a flag (starts with --)
|
||||
if strings.HasPrefix(part, "--") {
|
||||
// Remove the -- prefix
|
||||
flagName := part // Keep the '--' prefix in the map key for consistency? Or remove? Plan used remove.
|
||||
// flagName := strings.TrimPrefix(part, "--") // Alternative: remove prefix
|
||||
|
||||
// Check if the flag has a value
|
||||
if i+1 < len(parts) && !strings.HasPrefix(parts[i+1], "--") {
|
||||
// Next part is a value
|
||||
result[flagName] = parts[i+1]
|
||||
i++ // Skip the value in the next iteration
|
||||
} else {
|
||||
// Flag without value, treat as boolean true
|
||||
result[flagName] = "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// --- Rclone Store Methods ---
|
||||
|
||||
// GetRcloneCommands returns all rclone commands
|
||||
func (db *DB) GetRcloneCommands() ([]RcloneCommand, error) {
|
||||
var commands []RcloneCommand
|
||||
err := db.Find(&commands).Error
|
||||
return commands, err
|
||||
}
|
||||
|
||||
// GetRcloneCommand returns a specific rclone command by ID
|
||||
func (db *DB) GetRcloneCommand(id uint) (*RcloneCommand, error) {
|
||||
var command RcloneCommand
|
||||
err := db.First(&command, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &command, nil
|
||||
}
|
||||
|
||||
// GetRcloneCommandByName returns a specific rclone command by name
|
||||
func (db *DB) GetRcloneCommandByName(name string) (*RcloneCommand, error) {
|
||||
var command RcloneCommand
|
||||
err := db.Where("name = ?", name).First(&command).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &command, nil
|
||||
}
|
||||
|
||||
// GetRcloneCommandsInCategory returns all commands in a specific category
|
||||
func (db *DB) GetRcloneCommandsInCategory(category string) ([]RcloneCommand, error) {
|
||||
var commands []RcloneCommand
|
||||
err := db.Where("category = ?", category).Find(&commands).Error
|
||||
return commands, err
|
||||
}
|
||||
|
||||
// GetRcloneCommandFlag returns a specific flag by ID
|
||||
func (db *DB) GetRcloneCommandFlag(id uint) (*RcloneCommandFlag, error) {
|
||||
var flag RcloneCommandFlag
|
||||
err := db.First(&flag, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &flag, nil
|
||||
}
|
||||
|
||||
// GetRcloneCommandFlagByName returns a specific flag by name for a command
|
||||
func (db *DB) GetRcloneCommandFlagByName(commandID uint, name string) (*RcloneCommandFlag, error) {
|
||||
var flag RcloneCommandFlag
|
||||
err := db.Where("command_id = ? AND name = ?", commandID, name).First(&flag).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &flag, nil
|
||||
}
|
||||
|
||||
// GetRcloneCommandFlags returns all flags for a specific command
|
||||
func (db *DB) GetRcloneCommandFlags(commandID uint) ([]RcloneCommandFlag, error) {
|
||||
var flags []RcloneCommandFlag
|
||||
err := db.Where("command_id = ?", commandID).Find(&flags).Error
|
||||
return flags, err
|
||||
}
|
||||
|
||||
// GetRcloneCommandWithFlags returns a command with all its flags
|
||||
func (db *DB) GetRcloneCommandWithFlags(commandID uint) (*RcloneCommand, error) {
|
||||
var command RcloneCommand
|
||||
err := db.Preload("Flags").First(&command, commandID).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &command, nil
|
||||
}
|
||||
|
||||
// BuildRcloneCommand builds an rclone command string with the specified command and flags
|
||||
func (db *DB) BuildRcloneCommand(commandName string, flags map[string]string) (string, error) {
|
||||
// Get the command details
|
||||
command, err := db.GetRcloneCommandByName(commandName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("command not found: %s", commandName)
|
||||
}
|
||||
|
||||
// Start building the command string
|
||||
cmdStr := "rclone " + command.Name
|
||||
|
||||
// Get all flags for this command
|
||||
allFlags, err := db.GetRcloneCommandFlags(command.ID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get flags for command: %v", err)
|
||||
}
|
||||
|
||||
// Create a map of flag details for easy lookup
|
||||
flagDetails := make(map[string]RcloneCommandFlag)
|
||||
for _, f := range allFlags {
|
||||
flagDetails[f.Name] = f
|
||||
}
|
||||
|
||||
// Add the flags to the command
|
||||
for name, value := range flags {
|
||||
// Check if the flag exists for this command
|
||||
flag, exists := flagDetails[name]
|
||||
if !exists {
|
||||
// Allow passing flags not explicitly defined in the DB (e.g., global flags)
|
||||
// Consider adding validation or logging for unknown flags if stricter control is needed
|
||||
cmdStr += " " + name
|
||||
if value != "true" { // Assume boolean flags are passed as "true" if value is needed
|
||||
cmdStr += " " + value
|
||||
}
|
||||
continue
|
||||
// return "", fmt.Errorf("invalid flag for command %s: %s", commandName, name)
|
||||
}
|
||||
|
||||
// Handle different flag types
|
||||
switch flag.DataType {
|
||||
case "bool":
|
||||
if value == "true" {
|
||||
cmdStr += " " + flag.Name // Use flag.Name which includes '--'
|
||||
}
|
||||
default:
|
||||
cmdStr += " " + flag.Name + " " + value // Use flag.Name which includes '--'
|
||||
}
|
||||
}
|
||||
|
||||
return cmdStr, nil
|
||||
}
|
||||
|
||||
// ValidateRcloneFlags validates if the provided flags are valid for the command
|
||||
func (db *DB) ValidateRcloneFlags(commandName string, flags map[string]string) (bool, map[string]string) {
|
||||
// Initialize errors map
|
||||
errorsMap := make(map[string]string)
|
||||
|
||||
// Get the command details
|
||||
command, err := db.GetRcloneCommandByName(commandName)
|
||||
if err != nil {
|
||||
errorsMap["command"] = "Command not found: " + commandName
|
||||
return false, errorsMap
|
||||
}
|
||||
|
||||
// Get all flags for this command
|
||||
allFlags, err := db.GetRcloneCommandFlags(command.ID)
|
||||
if err != nil {
|
||||
errorsMap["command"] = "Failed to get flags for command"
|
||||
return false, errorsMap
|
||||
}
|
||||
|
||||
// Create a map of flag details for easy lookup
|
||||
flagDetails := make(map[string]RcloneCommandFlag)
|
||||
for _, f := range allFlags {
|
||||
flagDetails[f.Name] = f // Assuming Name includes '--' prefix
|
||||
}
|
||||
|
||||
// Check each provided flag
|
||||
for name, value := range flags {
|
||||
// Check if the flag exists for this command
|
||||
flag, exists := flagDetails[name]
|
||||
if !exists {
|
||||
// Allow unknown flags for now, but could add an error here if needed
|
||||
// errorsMap[name] = "Invalid flag for command " + commandName
|
||||
continue
|
||||
}
|
||||
|
||||
// Validate the flag value based on data type
|
||||
switch flag.DataType {
|
||||
case "int":
|
||||
if _, err := strconv.Atoi(value); err != nil {
|
||||
errorsMap[name] = "Value must be an integer"
|
||||
}
|
||||
case "float":
|
||||
if _, err := strconv.ParseFloat(value, 64); err != nil {
|
||||
errorsMap[name] = "Value must be a number"
|
||||
}
|
||||
case "bool":
|
||||
// For boolean flags passed in the map, the value should ideally be "true" or omitted
|
||||
// If present and not "true", it's likely an error or misuse.
|
||||
// Rclone CLI typically handles bool flags by presence/absence.
|
||||
// This validation might need refinement based on how flags are constructed before calling this.
|
||||
if value != "true" {
|
||||
// errorsMap[name] = "Boolean flag should have value 'true' or be omitted"
|
||||
}
|
||||
case "string":
|
||||
// Basic check: ensure value is not empty if flag requires a value
|
||||
// More complex validation (regex, length) could be added here
|
||||
if value == "" && flag.IsRequired { // Check if required string flags have values
|
||||
errorsMap[name] = "Value cannot be empty for required string flag"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for missing required flags
|
||||
for _, flag := range allFlags {
|
||||
if flag.IsRequired {
|
||||
if _, provided := flags[flag.Name]; !provided {
|
||||
// Check if the short name was provided instead
|
||||
shortNameProvided := false
|
||||
if flag.ShortName != "" {
|
||||
_, shortNameProvided = flags[flag.ShortName]
|
||||
}
|
||||
if !shortNameProvided {
|
||||
errorsMap[flag.Name] = "This flag is required"
|
||||
}
|
||||
} else if flag.DataType != "bool" && flags[flag.Name] == "" {
|
||||
// Required non-bool flags must have a value
|
||||
errorsMap[flag.Name] = "Value cannot be empty for required flag"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return len(errorsMap) == 0, errorsMap
|
||||
}
|
||||
|
||||
// GetRcloneCategories returns all unique categories of rclone commands
|
||||
func (db *DB) GetRcloneCategories() ([]string, error) {
|
||||
var categories []string
|
||||
err := db.Model(&RcloneCommand{}).Distinct("category").Pluck("category", &categories).Error
|
||||
return categories, err
|
||||
}
|
||||
|
||||
// GetRcloneCommandsByAdvanced returns commands filtered by their advanced status
|
||||
func (db *DB) GetRcloneCommandsByAdvanced(isAdvanced bool) ([]RcloneCommand, error) {
|
||||
var commands []RcloneCommand
|
||||
err := db.Where("is_advanced = ?", isAdvanced).Find(&commands).Error
|
||||
return commands, err
|
||||
}
|
||||
|
||||
// SearchRcloneCommands searches for commands by name or description
|
||||
func (db *DB) SearchRcloneCommands(query string) ([]RcloneCommand, error) {
|
||||
var commands []RcloneCommand
|
||||
searchQuery := "%" + query + "%"
|
||||
err := db.Where("name LIKE ? OR description LIKE ?", searchQuery, searchQuery).Find(&commands).Error
|
||||
return commands, err
|
||||
}
|
||||
|
||||
// GetRcloneCommandUsage returns a basic usage example for a command with its required flags
|
||||
func (db *DB) GetRcloneCommandUsage(commandID uint) (string, error) {
|
||||
command, err := db.GetRcloneCommandWithFlags(commandID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
usage := fmt.Sprintf("rclone %s [flags] <source> <dest>", command.Name)
|
||||
|
||||
// Add basic usage examples for required flags
|
||||
requiredFlags := []string{}
|
||||
for _, flag := range command.Flags {
|
||||
if flag.IsRequired {
|
||||
// Assuming GetUsageExample is defined on RcloneCommandFlag in rclone.go
|
||||
requiredFlags = append(requiredFlags, flag.GetUsageExample())
|
||||
}
|
||||
}
|
||||
|
||||
if len(requiredFlags) > 0 {
|
||||
usage += "\n\nRequired flags:\n " + strings.Join(requiredFlags, "\n ")
|
||||
}
|
||||
|
||||
return usage, nil
|
||||
}
|
||||
|
||||
// RenderRcloneCommandHelp generates a help text for a command with its flags
|
||||
func (db *DB) RenderRcloneCommandHelp(commandID uint) (string, error) {
|
||||
command, err := db.GetRcloneCommandWithFlags(commandID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Build the help text
|
||||
help := fmt.Sprintf("COMMAND: %s\n", command.Name)
|
||||
help += fmt.Sprintf("DESCRIPTION: %s\n\n", command.Description)
|
||||
help += "FLAGS:\n"
|
||||
|
||||
// Group flags by required status
|
||||
var requiredFlags, optionalFlags []RcloneCommandFlag
|
||||
for _, flag := range command.Flags {
|
||||
if flag.IsRequired {
|
||||
requiredFlags = append(requiredFlags, flag)
|
||||
} else {
|
||||
optionalFlags = append(optionalFlags, flag)
|
||||
}
|
||||
}
|
||||
|
||||
// Add required flags
|
||||
if len(requiredFlags) > 0 {
|
||||
help += " Required:\n"
|
||||
for _, flag := range requiredFlags {
|
||||
shortName := ""
|
||||
if flag.ShortName != "" {
|
||||
shortName = fmt.Sprintf(" (-%s)", flag.ShortName)
|
||||
}
|
||||
help += fmt.Sprintf(" %s%s - %s\n", flag.Name, shortName, flag.Description)
|
||||
if flag.DataType != "bool" && flag.DefaultValue != "" {
|
||||
help += fmt.Sprintf(" Default: %s\n", flag.DefaultValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add optional flags
|
||||
if len(optionalFlags) > 0 {
|
||||
help += "\n Optional:\n"
|
||||
for _, flag := range optionalFlags {
|
||||
shortName := ""
|
||||
if flag.ShortName != "" {
|
||||
shortName = fmt.Sprintf(" (-%s)", flag.ShortName)
|
||||
}
|
||||
help += fmt.Sprintf(" %s%s - %s\n", flag.Name, shortName, flag.Description)
|
||||
if flag.DataType != "bool" && flag.DefaultValue != "" {
|
||||
help += fmt.Sprintf(" Default: %s\n", flag.DefaultValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return help, nil
|
||||
}
|
||||
|
||||
// GetRcloneCommandFlagsMap returns all flags for a specific command as a map keyed by flag ID
|
||||
func (db *DB) GetRcloneCommandFlagsMap(commandID uint) (map[uint]RcloneCommandFlag, error) {
|
||||
flags, err := db.GetRcloneCommandFlags(commandID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
flagsMap := make(map[uint]RcloneCommandFlag)
|
||||
for _, flag := range flags {
|
||||
flagsMap[flag.ID] = flag
|
||||
}
|
||||
|
||||
return flagsMap, nil
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// --- Role Store Methods ---
|
||||
|
||||
// CreateRole creates a new role record
|
||||
func (db *DB) CreateRole(role *Role) error {
|
||||
return db.Create(role).Error
|
||||
}
|
||||
|
||||
// GetRole retrieves a role by ID, preloading permissions
|
||||
func (db *DB) GetRole(id uint) (*Role, error) {
|
||||
var role Role
|
||||
// Assuming Permissions are handled correctly by GORM or custom type
|
||||
err := db.First(&role, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &role, nil
|
||||
}
|
||||
|
||||
// GetRoleByName retrieves a role by name, preloading permissions
|
||||
func (db *DB) GetRoleByName(name string) (*Role, error) {
|
||||
var role Role
|
||||
err := db.Where("name = ?", name).First(&role).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &role, nil
|
||||
}
|
||||
|
||||
// UpdateRole updates an existing role record
|
||||
func (db *DB) UpdateRole(role *Role) error {
|
||||
// Use Omit Users to prevent GORM from trying to update the many2many relationship directly here
|
||||
return db.Omit("Users").Save(role).Error
|
||||
}
|
||||
|
||||
// DeleteRole deletes a role after checking dependencies and removing assignments
|
||||
func (db *DB) DeleteRole(id uint) error {
|
||||
var role Role
|
||||
if err := db.First(&role, id).Error; err != nil {
|
||||
return fmt.Errorf("role not found: %w", err)
|
||||
}
|
||||
|
||||
if role.IsSystemRole() {
|
||||
return errors.New("cannot delete system role")
|
||||
}
|
||||
|
||||
// Start transaction
|
||||
tx := db.Begin()
|
||||
if err := tx.Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Manually delete role assignments from the join table
|
||||
if err := tx.Exec("DELETE FROM user_roles WHERE role_id = ?", id).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("failed to delete role assignments: %w", err)
|
||||
}
|
||||
|
||||
// Delete the role itself
|
||||
if err := tx.Delete(&role).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("failed to delete role: %w", err)
|
||||
}
|
||||
|
||||
// Commit transaction
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
// ListRoles retrieves all roles
|
||||
func (db *DB) ListRoles() ([]Role, error) {
|
||||
var roles []Role
|
||||
err := db.Find(&roles).Error
|
||||
return roles, err
|
||||
}
|
||||
|
||||
// GetUserRoles retrieves all roles assigned to a specific user ID
|
||||
func (db *DB) GetUserRoles(userID uint) ([]Role, error) {
|
||||
var user User
|
||||
// Preload the Roles association
|
||||
if err := db.Preload("Roles").First(&user, userID).Error; err != nil {
|
||||
// Handle case where user might not be found vs. other errors
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fmt.Errorf("user with ID %d not found", userID)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get user %d roles: %w", userID, err)
|
||||
}
|
||||
return user.Roles, nil
|
||||
}
|
||||
|
||||
// AssignRoleToUser assigns a role to a user, handling the join table
|
||||
func (db *DB) AssignRoleToUser(roleID, userID, assignedByID uint) error {
|
||||
var role Role
|
||||
if err := db.First(&role, roleID).Error; err != nil {
|
||||
return fmt.Errorf("role with ID %d not found: %w", roleID, err)
|
||||
}
|
||||
var user User
|
||||
if err := db.First(&user, userID).Error; err != nil {
|
||||
return fmt.Errorf("user with ID %d not found: %w", userID, err)
|
||||
}
|
||||
|
||||
// Use GORM's Association API for many2many
|
||||
err := db.Model(&user).Association("Roles").Append(&role)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to assign role %d to user %d: %w", roleID, userID, err)
|
||||
}
|
||||
|
||||
// Optionally, log the assignment (consider moving audit logging to a dedicated service/hook)
|
||||
// db.Create(&AuditLog{...})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnassignRoleFromUser removes a role from a user, handling the join table
|
||||
func (db *DB) UnassignRoleFromUser(roleID, userID, unassignedByID uint) error {
|
||||
var role Role
|
||||
if err := db.First(&role, roleID).Error; err != nil {
|
||||
return fmt.Errorf("role with ID %d not found: %w", roleID, err)
|
||||
}
|
||||
var user User
|
||||
// Need to preload roles to check if the association exists before deleting
|
||||
if err := db.Preload("Roles").First(&user, userID).Error; err != nil {
|
||||
return fmt.Errorf("user with ID %d not found: %w", userID, err)
|
||||
}
|
||||
|
||||
// Use GORM's Association API for many2many deletion
|
||||
err := db.Model(&user).Association("Roles").Delete(&role)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unassign role %d from user %d: %w", roleID, userID, err)
|
||||
}
|
||||
|
||||
// Optionally, log the unassignment
|
||||
// db.Create(&AuditLog{...})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// TransferConfig holds the configuration for a data transfer operation
|
||||
type TransferConfig struct {
|
||||
ID uint `gorm:"primarykey"`
|
||||
Name string `gorm:"not null" form:"name"`
|
||||
SourceType string `gorm:"not null" form:"source_type"`
|
||||
SourcePath string `gorm:"not null" form:"source_path"`
|
||||
SourceHost string `form:"source_host"`
|
||||
SourcePort int `gorm:"default:22" form:"source_port"`
|
||||
SourceUser string `form:"source_user"`
|
||||
SourcePassword string `form:"source_password" gorm:"-"` // Not stored in DB, only used for form
|
||||
SourceKeyFile string `form:"source_key_file"`
|
||||
// S3 source fields
|
||||
SourceBucket string `form:"source_bucket"`
|
||||
SourceRegion string `form:"source_region"`
|
||||
SourceAccessKey string `form:"source_access_key"`
|
||||
SourceSecretKey string `form:"source_secret_key" gorm:"-"` // Not stored in DB, only used for form
|
||||
SourceEndpoint string `form:"source_endpoint"`
|
||||
// SMB source fields
|
||||
SourceShare string `form:"source_share"`
|
||||
SourceDomain string `form:"source_domain"`
|
||||
// FTP source fields
|
||||
SourcePassiveMode *bool `gorm:"default:true" form:"source_passive_mode"` // Already a pointer, no change needed here
|
||||
// OneDrive and Google Drive source fields
|
||||
SourceClientID string `form:"source_client_id"`
|
||||
SourceClientSecret string `form:"source_client_secret" gorm:"-"` // Not stored in DB, only used for form
|
||||
SourceDriveID string `form:"source_drive_id"` // For OneDrive
|
||||
SourceTeamDrive string `form:"source_team_drive"` // For Google Drive
|
||||
// Google Photos source fields
|
||||
SourceReadOnly *bool `form:"source_read_only"` // For Google Photos
|
||||
SourceStartYear int `form:"source_start_year"` // For Google Photos
|
||||
SourceIncludeArchived *bool `form:"source_include_archived"` // For Google Photos
|
||||
// General fields
|
||||
FilePattern string `gorm:"default:'*'" form:"file_pattern"`
|
||||
OutputPattern string `form:"output_pattern"` // Pattern for output filenames with date variables
|
||||
DestinationType string `gorm:"not null" form:"destination_type"`
|
||||
DestinationPath string `gorm:"not null" form:"destination_path"`
|
||||
DestHost string `form:"dest_host"`
|
||||
DestPort int `gorm:"default:22" form:"dest_port"`
|
||||
DestUser string `form:"dest_user"`
|
||||
DestPassword string `form:"dest_password" gorm:"-"` // Not stored in DB, only used for form
|
||||
DestKeyFile string `form:"dest_key_file"`
|
||||
// S3 destination fields
|
||||
DestBucket string `form:"dest_bucket"`
|
||||
DestRegion string `form:"dest_region"`
|
||||
DestAccessKey string `form:"dest_access_key"`
|
||||
DestSecretKey string `form:"dest_secret_key" gorm:"-"` // Not stored in DB, only used for form
|
||||
DestEndpoint string `form:"dest_endpoint"`
|
||||
// SMB destination fields
|
||||
DestShare string `form:"dest_share"`
|
||||
DestDomain string `form:"dest_domain"`
|
||||
// FTP destination fields
|
||||
DestPassiveMode *bool `gorm:"default:true" form:"dest_passive_mode"`
|
||||
// OneDrive and Google Drive destination fields
|
||||
DestClientID string `form:"dest_client_id"`
|
||||
DestClientSecret string `form:"dest_client_secret" gorm:"-"` // Not stored in DB, only used for form
|
||||
DestDriveID string `form:"dest_drive_id"` // For OneDrive
|
||||
DestTeamDrive string `form:"dest_team_drive"` // For Google Drive
|
||||
// Google Photos destination fields
|
||||
DestReadOnly *bool `form:"dest_read_only"` // For Google Photos
|
||||
DestStartYear int `form:"dest_start_year"` // For Google Photos
|
||||
DestIncludeArchived *bool `form:"dest_include_archived"` // For Google Photos
|
||||
// Security fields
|
||||
UseBuiltinAuthSource *bool `form:"use_builtin_auth_source"` // For Google and other OAuth services
|
||||
UseBuiltinAuthDest *bool `form:"use_builtin_auth_dest"` // For Google and other OAuth services
|
||||
GoogleDriveAuthenticated *bool // Whether Google Drive auth is completed
|
||||
// General fields
|
||||
ArchivePath string `form:"archive_path"`
|
||||
ArchiveEnabled *bool `gorm:"default:false" form:"archive_enabled"`
|
||||
RcloneFlags string `form:"rclone_flags"`
|
||||
// Rclone command fields
|
||||
CommandID uint `gorm:"default:1" form:"command_id"` // Default to 'copy' command ID (1)
|
||||
CommandFlags string `form:"command_flags"` // JSON string of selected flags
|
||||
CommandFlagValues string `form:"command_flag_values"` // JSON string of flag values by ID
|
||||
DeleteAfterTransfer *bool `gorm:"default:false" form:"delete_after_transfer"`
|
||||
SkipProcessedFiles *bool `gorm:"default:true" form:"skip_processed_files"`
|
||||
MaxConcurrentTransfers int `gorm:"default:4" form:"max_concurrent_transfers"` // Number of concurrent file transfers
|
||||
CreatedBy uint
|
||||
User User `gorm:"foreignkey:CreatedBy"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// --- TransferConfig Helper Methods ---
|
||||
|
||||
// GetSourcePassiveMode returns the value of SourcePassiveMode with a default if nil
|
||||
func (tc *TransferConfig) GetSourcePassiveMode() bool {
|
||||
if tc.SourcePassiveMode == nil {
|
||||
return true // Default to true if not set
|
||||
}
|
||||
return *tc.SourcePassiveMode
|
||||
}
|
||||
|
||||
// SetSourcePassiveMode sets the SourcePassiveMode field
|
||||
func (tc *TransferConfig) SetSourcePassiveMode(value bool) {
|
||||
tc.SourcePassiveMode = &value
|
||||
}
|
||||
|
||||
// GetDestPassiveMode returns the value of DestPassiveMode with a default if nil
|
||||
func (tc *TransferConfig) GetDestPassiveMode() bool {
|
||||
if tc.DestPassiveMode == nil {
|
||||
return true // Default to true if not set
|
||||
}
|
||||
return *tc.DestPassiveMode
|
||||
}
|
||||
|
||||
// SetDestPassiveMode sets the DestPassiveMode field
|
||||
func (tc *TransferConfig) SetDestPassiveMode(value bool) {
|
||||
tc.DestPassiveMode = &value
|
||||
}
|
||||
|
||||
// GetGoogleDriveAuthenticated returns whether the transfer config has been authenticated with Google Drive
|
||||
func (tc *TransferConfig) GetGoogleDriveAuthenticated() bool {
|
||||
return tc.GoogleDriveAuthenticated != nil && *tc.GoogleDriveAuthenticated
|
||||
}
|
||||
|
||||
// SetGoogleDriveAuthenticated sets the Google Drive authentication status
|
||||
func (tc *TransferConfig) SetGoogleDriveAuthenticated(value bool) {
|
||||
tc.GoogleDriveAuthenticated = &value
|
||||
}
|
||||
|
||||
// GetGoogleAuthenticated is an alias for GetGoogleDriveAuthenticated for better semantics when working with Google Photos
|
||||
func (tc *TransferConfig) GetGoogleAuthenticated() bool {
|
||||
return tc.GetGoogleDriveAuthenticated()
|
||||
}
|
||||
|
||||
// SetGoogleAuthenticated is an alias for SetGoogleDriveAuthenticated for better semantics when working with Google Photos
|
||||
func (tc *TransferConfig) SetGoogleAuthenticated(value bool) {
|
||||
tc.SetGoogleDriveAuthenticated(value)
|
||||
}
|
||||
|
||||
// GetArchiveEnabled returns the value of ArchiveEnabled with a default if nil
|
||||
func (tc *TransferConfig) GetArchiveEnabled() bool {
|
||||
if tc.ArchiveEnabled == nil {
|
||||
return false // Default to false if not set
|
||||
}
|
||||
return *tc.ArchiveEnabled
|
||||
}
|
||||
|
||||
// SetArchiveEnabled sets the ArchiveEnabled field
|
||||
func (tc *TransferConfig) SetArchiveEnabled(value bool) {
|
||||
tc.ArchiveEnabled = &value
|
||||
}
|
||||
|
||||
// GetDeleteAfterTransfer returns the value of DeleteAfterTransfer with a default if nil
|
||||
func (tc *TransferConfig) GetDeleteAfterTransfer() bool {
|
||||
if tc.DeleteAfterTransfer == nil {
|
||||
return false // Default to false if not set
|
||||
}
|
||||
return *tc.DeleteAfterTransfer
|
||||
}
|
||||
|
||||
// SetDeleteAfterTransfer sets the DeleteAfterTransfer field
|
||||
func (tc *TransferConfig) SetDeleteAfterTransfer(value bool) {
|
||||
tc.DeleteAfterTransfer = &value
|
||||
}
|
||||
|
||||
// GetSkipProcessedFiles returns the value of SkipProcessedFiles with a default if nil
|
||||
func (tc *TransferConfig) GetSkipProcessedFiles() bool {
|
||||
if tc.SkipProcessedFiles == nil {
|
||||
return true // Default to true if not set
|
||||
}
|
||||
return *tc.SkipProcessedFiles
|
||||
}
|
||||
|
||||
// SetSkipProcessedFiles sets the SkipProcessedFiles field
|
||||
func (tc *TransferConfig) SetSkipProcessedFiles(value bool) {
|
||||
tc.SkipProcessedFiles = &value
|
||||
}
|
||||
|
||||
// GetUseBuiltinAuthSource returns the value of UseBuiltinAuthSource with a default if nil
|
||||
func (tc *TransferConfig) GetUseBuiltinAuthSource() bool {
|
||||
if tc.UseBuiltinAuthSource == nil {
|
||||
return true // Default to true if not set
|
||||
}
|
||||
return *tc.UseBuiltinAuthSource
|
||||
}
|
||||
|
||||
// SetUseBuiltinAuthSource sets the UseBuiltinAuthSource field
|
||||
func (tc *TransferConfig) SetUseBuiltinAuthSource(value bool) {
|
||||
tc.UseBuiltinAuthSource = &value
|
||||
}
|
||||
|
||||
// GetUseBuiltinAuthDest returns the value of UseBuiltinAuthDest with a default if nil
|
||||
func (tc *TransferConfig) GetUseBuiltinAuthDest() bool {
|
||||
if tc.UseBuiltinAuthDest == nil {
|
||||
return true // Default to true if not set
|
||||
}
|
||||
return *tc.UseBuiltinAuthDest
|
||||
}
|
||||
|
||||
// SetUseBuiltinAuthDest sets the UseBuiltinAuthDest field
|
||||
func (tc *TransferConfig) SetUseBuiltinAuthDest(value bool) {
|
||||
tc.UseBuiltinAuthDest = &value
|
||||
}
|
||||
@@ -0,0 +1,505 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// --- TransferConfig Store Methods ---
|
||||
|
||||
// CreateTransferConfig creates a new transfer config record
|
||||
func (db *DB) CreateTransferConfig(config *TransferConfig) error {
|
||||
return db.Create(config).Error
|
||||
}
|
||||
|
||||
// GetTransferConfigs retrieves all transfer configs for a user
|
||||
func (db *DB) GetTransferConfigs(userID uint) ([]TransferConfig, error) {
|
||||
var configs []TransferConfig
|
||||
err := db.Where("created_by = ?", userID).Find(&configs).Error
|
||||
return configs, err
|
||||
}
|
||||
|
||||
// GetTransferConfig retrieves a single transfer config by ID
|
||||
func (db *DB) GetTransferConfig(id uint) (*TransferConfig, error) {
|
||||
var config TransferConfig
|
||||
err := db.First(&config, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// UpdateTransferConfig updates an existing transfer config record
|
||||
func (db *DB) UpdateTransferConfig(config *TransferConfig) error {
|
||||
return db.Save(config).Error
|
||||
}
|
||||
|
||||
// DeleteTransferConfig deletes a transfer config record after checking dependencies
|
||||
func (db *DB) DeleteTransferConfig(id uint) error {
|
||||
// First check if any jobs are using this config
|
||||
var count int64
|
||||
// Need to check both ConfigID and ConfigIDs list
|
||||
// This check might need refinement depending on how ConfigIDs is used reliably
|
||||
if err := db.Model(&Job{}).Where("config_id = ? OR config_ids LIKE ?", id, "%"+strconv.FormatUint(uint64(id), 10)+"%").Count(&count).Error; err != nil {
|
||||
return fmt.Errorf("failed to check for dependent jobs: %v", err)
|
||||
}
|
||||
if count > 0 {
|
||||
return fmt.Errorf("cannot delete config: %d jobs are using this configuration", count)
|
||||
}
|
||||
|
||||
// Delete the config
|
||||
return db.Delete(&TransferConfig{}, id).Error
|
||||
}
|
||||
|
||||
// GetConfigRclonePath returns the path to the rclone config file for a given transfer config
|
||||
func (db *DB) GetConfigRclonePath(config *TransferConfig) string {
|
||||
// Get data directory from environment or use default
|
||||
dataDir := os.Getenv("DATA_DIR")
|
||||
if dataDir == "" {
|
||||
dataDir = "./data"
|
||||
}
|
||||
|
||||
// Store configs in the data directory
|
||||
return filepath.Join(dataDir, "configs", fmt.Sprintf("config_%d.conf", config.ID))
|
||||
}
|
||||
|
||||
// GenerateRcloneConfig generates the rclone config file content based on TransferConfig
|
||||
// This function now primarily focuses on generating the content string or calling rclone config create
|
||||
func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
|
||||
configPath := db.GetConfigRclonePath(config)
|
||||
|
||||
// Get the directory part of the path
|
||||
configDir := filepath.Dir(configPath)
|
||||
|
||||
// Ensure configs directory exists
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create configs directory: %v", err)
|
||||
}
|
||||
|
||||
// Get the rclone path from the environment variable or use the default path
|
||||
rclonePath := os.Getenv("RCLONE_PATH")
|
||||
if rclonePath == "" {
|
||||
rclonePath = "rclone"
|
||||
}
|
||||
|
||||
sourceName := fmt.Sprintf("source_%d", config.ID)
|
||||
// Generate rclone config using rclone CLI for source
|
||||
switch config.SourceType {
|
||||
case "sftp":
|
||||
args := []string{
|
||||
"config", "create", sourceName, "sftp",
|
||||
"host", config.SourceHost,
|
||||
"user", config.SourceUser,
|
||||
"port", fmt.Sprintf("%d", config.SourcePort),
|
||||
"--non-interactive",
|
||||
"--config", configPath,
|
||||
"--log-level", "ERROR",
|
||||
}
|
||||
if config.SourcePassword != "" {
|
||||
args = append(args, "pass", config.SourcePassword)
|
||||
}
|
||||
if config.SourceKeyFile != "" {
|
||||
args = append(args, "key_file", config.SourceKeyFile)
|
||||
}
|
||||
cmd := exec.Command(rclonePath, args...)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to create source config (sftp): %v\nOutput: %s", err, output)
|
||||
}
|
||||
case "s3":
|
||||
args := []string{
|
||||
"config", "create", sourceName, "s3",
|
||||
"provider", "AWS", // Assuming AWS provider, adjust if needed
|
||||
"env_auth", "false",
|
||||
"access_key_id", config.SourceAccessKey,
|
||||
"secret_access_key", config.SourceSecretKey,
|
||||
"region", config.SourceRegion,
|
||||
"--non-interactive",
|
||||
"--config", configPath,
|
||||
"--log-level", "ERROR",
|
||||
}
|
||||
if config.SourceEndpoint != "" {
|
||||
args = append(args, "endpoint", config.SourceEndpoint)
|
||||
}
|
||||
cmd := exec.Command(rclonePath, args...)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to create source config (s3): %v\nOutput: %s", err, output)
|
||||
}
|
||||
case "minio":
|
||||
args := []string{
|
||||
"config", "create", sourceName, "s3",
|
||||
"provider", "Minio",
|
||||
"env_auth", "false",
|
||||
"access_key_id", config.SourceAccessKey,
|
||||
"secret_access_key", config.SourceSecretKey,
|
||||
"endpoint", config.SourceEndpoint,
|
||||
"--non-interactive",
|
||||
"--config", configPath,
|
||||
"--log-level", "ERROR",
|
||||
}
|
||||
// Add region if specified
|
||||
if config.SourceRegion != "" {
|
||||
args = append(args, "region", config.SourceRegion)
|
||||
}
|
||||
cmd := exec.Command(rclonePath, args...)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to create source config (minio): %v\nOutput: %s", err, output)
|
||||
}
|
||||
// ... (Add cases for other source types: b2, smb, ftp, webdav, nextcloud, onedrive, gdrive, gphotos) ...
|
||||
case "local":
|
||||
// For local source, ensure the section exists but might not need specific rclone config create
|
||||
content := fmt.Sprintf("[%s]\ntype = local\n\n", sourceName)
|
||||
if err := os.WriteFile(configPath, []byte(content), 0600); err != nil {
|
||||
return fmt.Errorf("failed to write source config (local): %v", err)
|
||||
}
|
||||
default:
|
||||
// Handle unknown or unsupported source types if necessary
|
||||
return fmt.Errorf("unsupported source type for rclone config generation: %s", config.SourceType)
|
||||
|
||||
}
|
||||
|
||||
destName := fmt.Sprintf("dest_%d", config.ID)
|
||||
// Generate rclone config using rclone CLI for destination
|
||||
switch config.DestinationType {
|
||||
case "sftp":
|
||||
args := []string{
|
||||
"config", "create", destName, "sftp",
|
||||
"host", config.DestHost,
|
||||
"user", config.DestUser,
|
||||
"port", fmt.Sprintf("%d", config.DestPort),
|
||||
"--non-interactive",
|
||||
"--config", configPath,
|
||||
"--log-level", "ERROR",
|
||||
}
|
||||
if config.DestPassword != "" {
|
||||
args = append(args, "pass", config.DestPassword)
|
||||
}
|
||||
if config.DestKeyFile != "" {
|
||||
args = append(args, "key_file", config.DestKeyFile)
|
||||
}
|
||||
cmd := exec.Command(rclonePath, args...)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to create destination config (sftp): %v\nOutput: %s", err, output)
|
||||
}
|
||||
case "s3":
|
||||
args := []string{
|
||||
"config", "create", destName, "s3",
|
||||
"provider", "AWS", // Assuming AWS provider
|
||||
"env_auth", "false",
|
||||
"access_key_id", config.DestAccessKey,
|
||||
"secret_access_key", config.DestSecretKey,
|
||||
"region", config.DestRegion,
|
||||
"--non-interactive",
|
||||
"--config", configPath,
|
||||
"--log-level", "ERROR",
|
||||
}
|
||||
if config.DestEndpoint != "" {
|
||||
args = append(args, "endpoint", config.DestEndpoint)
|
||||
}
|
||||
cmd := exec.Command(rclonePath, args...)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to create destination config (s3): %v\nOutput: %s", err, output)
|
||||
}
|
||||
case "minio":
|
||||
args := []string{
|
||||
"config", "create", destName, "s3",
|
||||
"provider", "Minio",
|
||||
"env_auth", "false",
|
||||
"access_key_id", config.DestAccessKey,
|
||||
"secret_access_key", config.DestSecretKey,
|
||||
"endpoint", config.DestEndpoint,
|
||||
"--non-interactive",
|
||||
"--config", configPath,
|
||||
"--log-level", "ERROR",
|
||||
}
|
||||
// Add region if specified
|
||||
if config.DestRegion != "" {
|
||||
args = append(args, "region", config.DestRegion)
|
||||
}
|
||||
cmd := exec.Command(rclonePath, args...)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to create destination config (minio): %v\nOutput: %s", err, output)
|
||||
}
|
||||
// ... (Add cases for other destination types: b2, smb, ftp, webdav, nextcloud, onedrive, gdrive, gphotos) ...
|
||||
case "local":
|
||||
// Append local config section
|
||||
content := fmt.Sprintf("\n[%s]\ntype = local\n", destName)
|
||||
f, err := os.OpenFile(configPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open config file for appending (local dest): %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
if _, err := f.WriteString(content); err != nil {
|
||||
return fmt.Errorf("failed to write destination config (local): %v", err)
|
||||
}
|
||||
default:
|
||||
// Handle unknown or unsupported destination types if necessary
|
||||
return fmt.Errorf("unsupported destination type for rclone config generation: %s", config.DestinationType)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StoreGoogleDriveToken stores the Google Drive auth token for a config
|
||||
func (db *DB) StoreGoogleDriveToken(configIDStr string, token string) error {
|
||||
configID, err := strconv.ParseUint(configIDStr, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid config ID: %v", err)
|
||||
}
|
||||
|
||||
config, err := db.GetTransferConfig(uint(configID))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get config: %v", err)
|
||||
}
|
||||
|
||||
authenticated := true
|
||||
config.GoogleDriveAuthenticated = &authenticated
|
||||
|
||||
if err := db.UpdateTransferConfig(config); err != nil {
|
||||
return fmt.Errorf("failed to update config: %v", err)
|
||||
}
|
||||
|
||||
configPath := db.GetConfigRclonePath(config)
|
||||
existingConfig := ""
|
||||
if _, err := os.Stat(configPath); err == nil {
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read existing config: %v", err)
|
||||
}
|
||||
existingConfig = string(data)
|
||||
}
|
||||
|
||||
configDir := filepath.Dir(configPath)
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create config directory: %v", err)
|
||||
}
|
||||
|
||||
destName := fmt.Sprintf("dest_%d", config.ID)
|
||||
newConfig := fmt.Sprintf("[%s]\ntype = drive\ntoken = %s\n", destName, token)
|
||||
|
||||
if config.DestClientID != "" && config.DestClientSecret != "" {
|
||||
newConfig += fmt.Sprintf("client_id = %s\nclient_secret = %s\n", config.DestClientID, config.DestClientSecret)
|
||||
}
|
||||
if config.DestDriveID != "" {
|
||||
newConfig += fmt.Sprintf("root_folder_id = %s\n", config.DestDriveID)
|
||||
}
|
||||
if config.DestTeamDrive != "" {
|
||||
newConfig += fmt.Sprintf("team_drive = %s\n", config.DestTeamDrive)
|
||||
}
|
||||
|
||||
var content string
|
||||
sectionHeader := fmt.Sprintf("[%s]", destName)
|
||||
if strings.Contains(existingConfig, sectionHeader) {
|
||||
parts := strings.SplitN(existingConfig, sectionHeader, 2)
|
||||
nextSectionIdx := strings.Index(parts[1], "[")
|
||||
if nextSectionIdx != -1 {
|
||||
content = parts[0] + newConfig + parts[1][nextSectionIdx:]
|
||||
} else {
|
||||
content = parts[0] + newConfig
|
||||
}
|
||||
} else {
|
||||
content = existingConfig + "\n" + newConfig
|
||||
}
|
||||
|
||||
if err := os.WriteFile(configPath, []byte(content), 0600); err != nil {
|
||||
return fmt.Errorf("failed to write config: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateRcloneConfigWithToken generates a rclone config file for a transfer config with a provided token
|
||||
// Note: This seems partially redundant with StoreGoogleDriveToken and GenerateRcloneConfig. Consolidate if possible.
|
||||
func (db *DB) GenerateRcloneConfigWithToken(config *TransferConfig, token string) error {
|
||||
configPath := db.GetConfigRclonePath(config)
|
||||
if configPath == "" {
|
||||
return fmt.Errorf("failed to get config path")
|
||||
}
|
||||
|
||||
token = strings.TrimSpace(token)
|
||||
token = strings.ReplaceAll(token, "\n", "")
|
||||
token = strings.ReplaceAll(token, "\r", "")
|
||||
|
||||
var configType, section, clientID, clientSecret string
|
||||
var readOnly, includeArchived *bool
|
||||
var startYear int
|
||||
|
||||
// Determine if source or destination needs token update
|
||||
if config.DestinationType == "gdrive" || config.DestinationType == "gphotos" {
|
||||
configType = config.DestinationType
|
||||
section = "dest"
|
||||
clientID = config.DestClientID
|
||||
clientSecret = config.DestClientSecret
|
||||
readOnly = config.DestReadOnly
|
||||
startYear = config.DestStartYear
|
||||
includeArchived = config.DestIncludeArchived
|
||||
} else if config.SourceType == "gdrive" || config.SourceType == "gphotos" {
|
||||
configType = config.SourceType
|
||||
section = "source"
|
||||
clientID = config.SourceClientID
|
||||
clientSecret = config.SourceClientSecret
|
||||
readOnly = config.SourceReadOnly
|
||||
startYear = config.SourceStartYear
|
||||
includeArchived = config.SourceIncludeArchived
|
||||
} else {
|
||||
return fmt.Errorf("config is not for Google Drive or Google Photos")
|
||||
}
|
||||
|
||||
contentBytes, err := os.ReadFile(configPath)
|
||||
if err != nil && !os.IsNotExist(err) { // Allow file not existing yet
|
||||
return fmt.Errorf("failed to read config file: %v", err)
|
||||
}
|
||||
content := string(contentBytes)
|
||||
|
||||
var sectionContent string
|
||||
sectionHeader := fmt.Sprintf("[%s_%d]", section, config.ID)
|
||||
|
||||
if configType == "gdrive" {
|
||||
sectionContent = sectionHeader + "\ntype = drive\n"
|
||||
if clientID != "" {
|
||||
sectionContent += fmt.Sprintf("client_id = %s\n", clientID)
|
||||
}
|
||||
if clientSecret != "" {
|
||||
sectionContent += fmt.Sprintf("client_secret = %s\n", clientSecret)
|
||||
}
|
||||
sectionContent += fmt.Sprintf("token = %s\n", token)
|
||||
if section == "source" && config.SourceTeamDrive != "" {
|
||||
sectionContent += fmt.Sprintf("team_drive = %s\n", config.SourceTeamDrive)
|
||||
}
|
||||
if section == "dest" && config.DestTeamDrive != "" {
|
||||
sectionContent += fmt.Sprintf("team_drive = %s\n", config.DestTeamDrive)
|
||||
}
|
||||
if section == "dest" && config.DestDriveID != "" {
|
||||
sectionContent += fmt.Sprintf("root_folder_id = %s\n", config.DestDriveID)
|
||||
} // Use DestDriveID for root_folder_id
|
||||
} else if configType == "gphotos" {
|
||||
sectionContent = sectionHeader + "\ntype = google photos\n"
|
||||
if clientID != "" {
|
||||
sectionContent += fmt.Sprintf("client_id = %s\n", clientID)
|
||||
}
|
||||
if clientSecret != "" {
|
||||
sectionContent += fmt.Sprintf("client_secret = %s\n", clientSecret)
|
||||
}
|
||||
sectionContent += fmt.Sprintf("token = %s\n", token)
|
||||
if readOnly != nil && *readOnly {
|
||||
sectionContent += "read_only = true\n"
|
||||
}
|
||||
if startYear > 0 {
|
||||
sectionContent += fmt.Sprintf("start_year = %d\n", startYear)
|
||||
}
|
||||
if includeArchived != nil && *includeArchived {
|
||||
sectionContent += "include_archived = true\n"
|
||||
}
|
||||
}
|
||||
|
||||
// Replace or append logic
|
||||
sectionPattern := regexp.MustCompile(fmt.Sprintf(`(?m)^%s[^\[]*`, regexp.QuoteMeta(sectionHeader))) // Match section start to next section or EOF
|
||||
if sectionPattern.MatchString(content) {
|
||||
content = sectionPattern.ReplaceAllString(content, sectionContent)
|
||||
} else {
|
||||
if content != "" && !strings.HasSuffix(content, "\n\n") { // Ensure separation
|
||||
if !strings.HasSuffix(content, "\n") {
|
||||
content += "\n"
|
||||
}
|
||||
content += "\n"
|
||||
}
|
||||
content += sectionContent
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
configDir := filepath.Dir(configPath)
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create config directory: %v", err)
|
||||
}
|
||||
|
||||
// Write the updated config file
|
||||
if err := os.WriteFile(configPath, []byte(content), 0600); err != nil { // Use 0600 for sensitive files
|
||||
return fmt.Errorf("failed to write updated config file: %v", err)
|
||||
}
|
||||
|
||||
// Update the authentication status in DB
|
||||
authenticated := true
|
||||
if config.DestinationType == "gdrive" || config.DestinationType == "gphotos" {
|
||||
config.SetGoogleAuthenticated(authenticated)
|
||||
} else if config.SourceType == "gdrive" || config.SourceType == "gphotos" {
|
||||
config.SetGoogleAuthenticated(authenticated)
|
||||
}
|
||||
// Persist the change (assuming UpdateTransferConfig saves the whole object)
|
||||
if err := db.UpdateTransferConfig(config); err != nil {
|
||||
return fmt.Errorf("failed to update config authentication status: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetGDriveCredentialsFromConfig extracts Google Drive client ID and secret from an existing rclone config file
|
||||
func (db *DB) GetGDriveCredentialsFromConfig(config *TransferConfig) (string, string) {
|
||||
configPath := db.GetConfigRclonePath(config)
|
||||
if configPath == "" {
|
||||
return "", ""
|
||||
}
|
||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
lines := strings.Split(string(content), "\n")
|
||||
sourceSectionName := fmt.Sprintf("[source_%d]", config.ID)
|
||||
destSectionName := fmt.Sprintf("[dest_%d]", config.ID)
|
||||
var inSourceSection, inDestSection bool
|
||||
var sourceClientID, sourceClientSecret, destClientID, destClientSecret string
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||
inSourceSection = line == sourceSectionName
|
||||
inDestSection = line == destSectionName
|
||||
continue
|
||||
}
|
||||
if inSourceSection {
|
||||
if strings.HasPrefix(line, "client_id") {
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
sourceClientID = strings.TrimSpace(parts[1])
|
||||
}
|
||||
} else if strings.HasPrefix(line, "client_secret") {
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
sourceClientSecret = strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
if inDestSection {
|
||||
if strings.HasPrefix(line, "client_id") {
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
destClientID = strings.TrimSpace(parts[1])
|
||||
}
|
||||
} else if strings.HasPrefix(line, "client_secret") {
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
destClientSecret = strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
if sourceClientID != "" && sourceClientSecret != "" && destClientID != "" && destClientSecret != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if destClientID != "" && destClientSecret != "" {
|
||||
return destClientID, destClientSecret
|
||||
}
|
||||
if sourceClientID != "" && sourceClientSecret != "" {
|
||||
return sourceClientID, sourceClientSecret
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// User represents a user account in the system
|
||||
type User struct {
|
||||
ID uint `gorm:"primarykey"`
|
||||
Email string `gorm:"unique;not null"`
|
||||
PasswordHash string `gorm:"not null"`
|
||||
IsAdmin *bool `gorm:"default:false"`
|
||||
LastPasswordChange time.Time
|
||||
FailedLoginAttempts int `gorm:"default:0"`
|
||||
AccountLocked *bool `gorm:"default:false"`
|
||||
LockoutUntil *time.Time
|
||||
Theme string `gorm:"default:'light'"`
|
||||
TwoFactorSecret string `gorm:"type:varchar(32)"`
|
||||
TwoFactorEnabled bool `gorm:"default:false"`
|
||||
BackupCodes string `gorm:"type:text"` // Comma-separated backup codes
|
||||
Roles []Role `gorm:"many2many:user_roles"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// PasswordHistory stores previous passwords for a user
|
||||
type PasswordHistory struct {
|
||||
ID uint `gorm:"primarykey"`
|
||||
UserID uint `gorm:"not null"`
|
||||
User User `gorm:"foreignkey:UserID"`
|
||||
PasswordHash string `gorm:"not null"`
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// PasswordResetToken stores tokens for password reset requests
|
||||
type PasswordResetToken struct {
|
||||
ID uint `gorm:"primarykey"`
|
||||
UserID uint `gorm:"not null"`
|
||||
User User `gorm:"foreignkey:UserID"`
|
||||
Token string `gorm:"not null"`
|
||||
ExpiresAt time.Time `gorm:"not null"`
|
||||
Used *bool `gorm:"default:false"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// --- User Helper Methods ---
|
||||
|
||||
// GetIsAdmin returns the value of IsAdmin with a default if nil
|
||||
func (u *User) GetIsAdmin() bool {
|
||||
if u.IsAdmin == nil {
|
||||
return false // Default to false if not set
|
||||
}
|
||||
return *u.IsAdmin
|
||||
}
|
||||
|
||||
// SetIsAdmin sets the IsAdmin field
|
||||
func (u *User) SetIsAdmin(value bool) {
|
||||
u.IsAdmin = &value
|
||||
}
|
||||
|
||||
// GetAccountLocked returns the value of AccountLocked with a default if nil
|
||||
func (u *User) GetAccountLocked() bool {
|
||||
if u.AccountLocked == nil {
|
||||
return false // Default to false if not set
|
||||
}
|
||||
return *u.AccountLocked
|
||||
}
|
||||
|
||||
// SetAccountLocked sets the AccountLocked field
|
||||
func (u *User) SetAccountLocked(value bool) {
|
||||
u.AccountLocked = &value
|
||||
}
|
||||
|
||||
// HasRole checks if the user has a specific role
|
||||
func (u *User) HasRole(roleName string) bool {
|
||||
for _, role := range u.Roles {
|
||||
if role.Name == roleName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasPermission checks if the user has a specific permission through any of their roles
|
||||
func (u *User) HasPermission(permission string) bool {
|
||||
for _, role := range u.Roles {
|
||||
if role.HasPermission(permission) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetRoles returns all roles assigned to the user
|
||||
// Note: This requires preloading Roles when fetching the user
|
||||
func (u *User) GetRoles(tx *gorm.DB) ([]Role, error) {
|
||||
var roles []Role
|
||||
err := tx.Model(u).Association("Roles").Find(&roles)
|
||||
return roles, err
|
||||
}
|
||||
|
||||
// AssignRole assigns a role to the user
|
||||
func (u *User) AssignRole(tx *gorm.DB, roleID uint, assignedByID uint) error {
|
||||
var role Role
|
||||
if err := tx.First(&role, roleID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// Assuming Role struct has AssignToUser method (from role.go)
|
||||
return role.AssignToUser(tx, u.ID, assignedByID)
|
||||
}
|
||||
|
||||
// UnassignRole removes a role from the user
|
||||
func (u *User) UnassignRole(tx *gorm.DB, roleID uint, unassignedByID uint) error {
|
||||
var role Role
|
||||
if err := tx.First(&role, roleID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// Assuming Role struct has UnassignFromUser method (from role.go)
|
||||
return role.UnassignFromUser(tx, u.ID, unassignedByID)
|
||||
}
|
||||
|
||||
// SetPassword sets the user's password with secure hashing
|
||||
func (u *User) SetPassword(password string) error {
|
||||
// Validate password length
|
||||
if len(password) < 8 {
|
||||
return fmt.Errorf("password must be at least 8 characters long")
|
||||
}
|
||||
|
||||
// Hash the password using bcrypt
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to hash password: %w", err)
|
||||
}
|
||||
|
||||
// Store the hashed password
|
||||
u.PasswordHash = string(hashedPassword)
|
||||
u.LastPasswordChange = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckPassword verifies if the provided password matches the stored hash
|
||||
func (u *User) CheckPassword(password string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// --- PasswordResetToken Helper Methods ---
|
||||
|
||||
// GetUsed returns the value of Used with a default if nil
|
||||
func (t *PasswordResetToken) GetUsed() bool {
|
||||
if t.Used == nil {
|
||||
return false // Default to false if not set
|
||||
}
|
||||
return *t.Used
|
||||
}
|
||||
|
||||
// SetUsed sets the Used field
|
||||
func (t *PasswordResetToken) SetUsed(value bool) {
|
||||
t.Used = &value
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// --- User Store Methods ---
|
||||
|
||||
// CreateUser creates a new user record
|
||||
func (db *DB) CreateUser(user *User) error {
|
||||
return db.Create(user).Error
|
||||
}
|
||||
|
||||
// GetUserByEmail retrieves a user by their email address
|
||||
func (db *DB) GetUserByEmail(email string) (*User, error) {
|
||||
var user User
|
||||
// Preload Roles to ensure they are available for permission checks
|
||||
err := db.Preload("Roles").Where("email = ?", email).First(&user).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetUserByID retrieves a user by their ID
|
||||
func (db *DB) GetUserByID(id uint) (*User, error) {
|
||||
var user User
|
||||
// Preload Roles
|
||||
err := db.Preload("Roles").First(&user, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// UpdateUser updates an existing user record
|
||||
func (db *DB) UpdateUser(user *User) error {
|
||||
// Use Omit to prevent accidentally changing Roles association directly
|
||||
// Role assignments should use AssignRole/UnassignRole methods
|
||||
return db.Omit("Roles").Save(user).Error
|
||||
}
|
||||
|
||||
// --- PasswordResetToken Store Methods ---
|
||||
|
||||
// CreatePasswordResetToken creates a new password reset token record
|
||||
func (db *DB) CreatePasswordResetToken(token *PasswordResetToken) error {
|
||||
return db.Create(token).Error
|
||||
}
|
||||
|
||||
// GetPasswordResetToken retrieves a valid, unused password reset token
|
||||
func (db *DB) GetPasswordResetToken(token string) (*PasswordResetToken, error) {
|
||||
var resetToken PasswordResetToken
|
||||
err := db.Where("token = ? AND used = ? AND expires_at > ?", token, false, time.Now()).First(&resetToken).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resetToken, nil
|
||||
}
|
||||
|
||||
// MarkPasswordResetTokenAsUsed marks a password reset token as used
|
||||
func (db *DB) MarkPasswordResetTokenAsUsed(tokenID uint) error {
|
||||
return db.Model(&PasswordResetToken{}).Where("id = ?", tokenID).Update("used", true).Error
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/starfleetcptn/gomft/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func createTestConfig(enabled bool) *config.Config {
|
||||
return &config.Config{
|
||||
BaseURL: "http://localhost:8080",
|
||||
Email: config.EmailConfig{
|
||||
Enabled: enabled,
|
||||
Host: "smtp.example.com",
|
||||
Port: 587,
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
FromEmail: "noreply@example.com",
|
||||
FromName: "GoMFT Test",
|
||||
RequireAuth: true,
|
||||
EnableTLS: true,
|
||||
ReplyTo: "support@example.com",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewService(t *testing.T) {
|
||||
cfg := createTestConfig(true)
|
||||
service := NewService(cfg)
|
||||
require.NotNil(t, service)
|
||||
assert.Equal(t, cfg, service.Config)
|
||||
}
|
||||
|
||||
func TestGeneratePasswordResetEmailHTML(t *testing.T) {
|
||||
cfg := createTestConfig(true)
|
||||
service := NewService(cfg)
|
||||
|
||||
testUsername := "testuser"
|
||||
testResetLink := "http://localhost:8080/reset-password?token=testtoken123"
|
||||
testAppName := "GoMFT"
|
||||
testYear := time.Now().Year()
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Username": testUsername,
|
||||
"ResetLink": testResetLink,
|
||||
"AppName": testAppName,
|
||||
"Year": testYear,
|
||||
"ExpiresHours": 0.25,
|
||||
}
|
||||
|
||||
htmlContent, err := service.generatePasswordResetEmailHTML(data)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, htmlContent)
|
||||
|
||||
// Basic checks for content presence
|
||||
assert.Contains(t, htmlContent, "Reset Your Password")
|
||||
assert.Contains(t, htmlContent, fmt.Sprintf("Hello %s", testUsername))
|
||||
assert.Contains(t, htmlContent, testResetLink) // Check link appears (both in button and text)
|
||||
assert.Contains(t, htmlContent, fmt.Sprintf("href=\"%s\"", testResetLink))
|
||||
assert.Contains(t, htmlContent, fmt.Sprintf("© %d %s", testYear, testAppName))
|
||||
assert.Contains(t, htmlContent, "This link will expire in 15 minutes.")
|
||||
|
||||
// Test without username
|
||||
dataNoUser := map[string]interface{}{
|
||||
"ResetLink": testResetLink,
|
||||
"AppName": testAppName,
|
||||
"Year": testYear,
|
||||
"ExpiresHours": 0.25,
|
||||
}
|
||||
htmlContentNoUser, err := service.generatePasswordResetEmailHTML(dataNoUser)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, htmlContentNoUser, "Hello,") // Should just say Hello,
|
||||
assert.NotContains(t, htmlContentNoUser, fmt.Sprintf("Hello %s", testUsername))
|
||||
}
|
||||
|
||||
func TestGenerateTestEmailHTML(t *testing.T) {
|
||||
cfg := createTestConfig(true)
|
||||
service := NewService(cfg)
|
||||
|
||||
testSubject := "My Test Subject"
|
||||
testMessage := "This is the test message body."
|
||||
testAppName := "GoMFT"
|
||||
testYear := time.Now().Year()
|
||||
testCurrentTime := time.Now().Format(time.RFC1123Z) // Use the same format
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Subject": testSubject,
|
||||
"Message": testMessage,
|
||||
"AppName": testAppName,
|
||||
"Year": testYear,
|
||||
"SMTPServer": cfg.Email.Host,
|
||||
"SMTPPort": cfg.Email.Port,
|
||||
"FromEmail": cfg.Email.FromEmail,
|
||||
"CurrentTime": testCurrentTime,
|
||||
}
|
||||
|
||||
htmlContent, err := service.generateTestEmailHTML(data)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, htmlContent)
|
||||
|
||||
// Basic checks for content presence
|
||||
assert.Contains(t, htmlContent, fmt.Sprintf("<title>%s</title>", testSubject))
|
||||
assert.Contains(t, htmlContent, fmt.Sprintf("<h1>%s</h1>", testSubject))
|
||||
assert.Contains(t, htmlContent, testMessage)
|
||||
assert.Contains(t, htmlContent, fmt.Sprintf("%s:%d", cfg.Email.Host, cfg.Email.Port))
|
||||
assert.Contains(t, htmlContent, cfg.Email.FromEmail)
|
||||
assert.Contains(t, htmlContent, testCurrentTime)
|
||||
assert.Contains(t, htmlContent, fmt.Sprintf("© %d %s", testYear, testAppName))
|
||||
}
|
||||
|
||||
func TestSendPasswordResetEmail_Disabled(t *testing.T) {
|
||||
cfg := createTestConfig(false) // Email disabled
|
||||
service := NewService(cfg)
|
||||
|
||||
toEmail := "test@example.com"
|
||||
username := "testuser"
|
||||
resetToken := "disabledtoken123"
|
||||
|
||||
err := service.SendPasswordResetEmail(toEmail, username, resetToken)
|
||||
require.Error(t, err)
|
||||
|
||||
expectedErrorSubstr := fmt.Sprintf("email service is disabled, reset link would be: %s/reset-password?token=%s",
|
||||
cfg.BaseURL, resetToken)
|
||||
assert.Contains(t, err.Error(), expectedErrorSubstr)
|
||||
}
|
||||
|
||||
func TestSendTestEmail_Disabled(t *testing.T) {
|
||||
cfg := createTestConfig(false) // Email disabled
|
||||
service := NewService(cfg)
|
||||
|
||||
toEmail := "test@example.com"
|
||||
|
||||
err := service.SendTestEmail(toEmail, "Test Subject", "Test Message")
|
||||
require.Error(t, err)
|
||||
assert.EqualError(t, err, "email service is disabled")
|
||||
}
|
||||
|
||||
// --- Placeholder/TODO for more complex tests ---
|
||||
|
||||
// TODO: TestSendPasswordResetEmail_Enabled - Requires mocking sendEmail or SMTP interactions
|
||||
// TODO: TestSendTestEmail_Enabled - Requires mocking sendEmail or SMTP interactions
|
||||
// TODO: TestSendEmail - Requires extensive mocking of net/smtp package
|
||||
|
||||
// Example structure for testing enabled path (without actual sending/mocking)
|
||||
// This verifies the function prepares the correct data before calling sendEmail
|
||||
func TestSendPasswordResetEmail_Enabled_DataPreparation(t *testing.T) {
|
||||
cfg := createTestConfig(true)
|
||||
service := NewService(cfg)
|
||||
|
||||
// We need a way to intercept the call to sendEmail or verify its inputs
|
||||
// For now, we just check that no error occurs up to that point
|
||||
// and that the HTML generation works (implicitly tested by TestGeneratePasswordResetEmailHTML)
|
||||
|
||||
toEmail := "recipient@example.com"
|
||||
username := "testuser-enabled"
|
||||
resetToken := "enabledtoken456"
|
||||
|
||||
// If generatePasswordResetEmailHTML works, this call should proceed
|
||||
// without error until the actual sendEmail call (which we aren't testing here)
|
||||
// A full test would mock sendEmail and verify the arguments passed to it.
|
||||
err := service.SendPasswordResetEmail(toEmail, username, resetToken)
|
||||
|
||||
// In a real scenario without mocking, this might fail if SMTP connection fails.
|
||||
// For this basic check, we assume HTML generation is the main potential failure point *before* sendEmail.
|
||||
// If TestGeneratePasswordResetEmailHTML passes, we expect no error *from generation*.
|
||||
// We cannot assert assert.NoError(t, err) reliably without mocking sendEmail.
|
||||
t.Logf("SendPasswordResetEmail (enabled) returned: %v (expected success or SMTP error)", err)
|
||||
// Asserting that the error, if any, is NOT related to template generation could be a weak check.
|
||||
if err != nil {
|
||||
assert.False(t, strings.Contains(err.Error(), "template"), "Error should be SMTP related, not template related")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendTestEmail_Enabled_DataPreparation(t *testing.T) {
|
||||
cfg := createTestConfig(true)
|
||||
service := NewService(cfg)
|
||||
|
||||
toEmail := "recipient@example.com"
|
||||
subject := "Specific Test Subject"
|
||||
message := "Specific test message."
|
||||
|
||||
// Test with specific subject and message
|
||||
err := service.SendTestEmail(toEmail, subject, message)
|
||||
t.Logf("SendTestEmail (enabled, specific) returned: %v (expected success or SMTP error)", err)
|
||||
if err != nil {
|
||||
assert.False(t, strings.Contains(err.Error(), "template"), "Error should be SMTP related, not template related")
|
||||
}
|
||||
|
||||
// Test with default subject and message
|
||||
errDefault := service.SendTestEmail(toEmail, "", "")
|
||||
t.Logf("SendTestEmail (enabled, default) returned: %v (expected success or SMTP error)", errDefault)
|
||||
if errDefault != nil {
|
||||
assert.False(t, strings.Contains(errDefault.Error(), "template"), "Error should be SMTP related, not template related")
|
||||
}
|
||||
// A full test would mock sendEmail and verify the subject/message passed (checking defaults).
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user