512 lines
28 KiB
Vue
512 lines
28 KiB
Vue
<template>
|
|
<form @submit.prevent="handleSubmit" class="space-y-6 text-foreground"> <!-- Form container with spacing -->
|
|
<h3 class="text-lg font-semibold mb-4 pb-2 border-b border-border"> <!-- Title -->
|
|
{{ isEditing ? $t('settings.notifications.form.editTitle') : $t('settings.notifications.form.addTitle') }}
|
|
</h3>
|
|
|
|
<!-- General Settings -->
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label for="setting-name" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.name') }}</label>
|
|
<input type="text" id="setting-name" v-model="formData.name" required
|
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary">
|
|
</div>
|
|
|
|
<div class="flex items-center">
|
|
<input type="checkbox" id="setting-enabled" v-model="formData.enabled"
|
|
class="h-4 w-4 rounded border-border text-primary focus:ring-primary mr-2 cursor-pointer">
|
|
<label for="setting-enabled" class="text-sm text-foreground cursor-pointer">{{ $t('common.enabled') }}</label>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="setting-channel-type" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.channelType') }}</label>
|
|
<select id="setting-channel-type" v-model="formData.channel_type" required :disabled="isEditing"
|
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary appearance-none bg-no-repeat bg-right pr-8 disabled:opacity-70 disabled:bg-header/50"
|
|
style="background-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 16 16\'%3e%3cpath fill=\'none\' stroke=\'%236c757d\' stroke-linecap=\'round\' stroke-linejoin=\'round\' stroke-width=\'2\' d=\'M2 5l6 6 6-6\'/%3e%3c/svg%3e'); background-position: right 0.75rem center; background-size: 16px 12px;">
|
|
<option value="webhook">{{ $t('settings.notifications.types.webhook') }}</option>
|
|
<option value="email">{{ $t('settings.notifications.types.email') }}</option>
|
|
<option value="telegram">{{ $t('settings.notifications.types.telegram') }}</option>
|
|
</select>
|
|
<small v-if="isEditing" class="block mt-1 text-xs text-text-secondary">{{ $t('settings.notifications.form.channelTypeEditNote') }}</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Channel Specific Config -->
|
|
<div class="border border-border rounded-md p-4 mt-4 bg-header/30 space-y-4"> <!-- Config section container -->
|
|
<h4 class="text-base font-semibold mb-3 pb-2 border-b border-border/50"> <!-- Config section title -->
|
|
{{ $t(`settings.notifications.types.${formData.channel_type}`) }} {{ $t('common.settings') }}
|
|
</h4>
|
|
|
|
<!-- Webhook Config -->
|
|
<div v-if="formData.channel_type === 'webhook'" class="space-y-4">
|
|
<div>
|
|
<label for="webhook-url" class="block text-sm font-medium text-text-secondary mb-1">URL</label>
|
|
<input type="url" id="webhook-url" v-model="webhookConfig.url" required
|
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary">
|
|
</div>
|
|
<div>
|
|
<label for="webhook-method" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.webhookMethod') }}</label>
|
|
<select id="webhook-method" v-model="webhookConfig.method"
|
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary appearance-none bg-no-repeat bg-right pr-8"
|
|
style="background-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 16 16\'%3e%3cpath fill=\'none\' stroke=\'%236c757d\' stroke-linecap=\'round\' stroke-linejoin=\'round\' stroke-width=\'2\' d=\'M2 5l6 6 6-6\'/%3e%3c/svg%3e'); background-position: right 0.75rem center; background-size: 16px 12px;">
|
|
<option value="POST">POST</option>
|
|
<option value="GET">GET</option>
|
|
<option value="PUT">PUT</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label for="webhook-headers" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.webhookHeaders') }} (JSON)</label>
|
|
<textarea id="webhook-headers" v-model="webhookHeadersString" rows="3" placeholder='{"Content-Type": "application/json", "Authorization": "Bearer ..."}'
|
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary font-mono text-sm"></textarea>
|
|
<small v-if="headerError" class="block mt-1 text-xs text-error">{{ headerError }}</small> <!-- Use text-error -->
|
|
</div>
|
|
<div>
|
|
<label for="webhook-body" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.webhookBodyTemplate') }}</label>
|
|
<textarea id="webhook-body" v-model="webhookConfig.bodyTemplate" rows="3" :placeholder="`${$t('settings.notifications.form.webhookBodyPlaceholder')} {event}, {timestamp}, {details}`"
|
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary font-mono text-sm"></textarea>
|
|
<small class="block mt-1 text-xs text-text-secondary">{{ $t('settings.notifications.form.templateHelp') }} {event}, {timestamp}, {details} (JSON string)</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Email Config -->
|
|
<div v-if="formData.channel_type === 'email'" class="space-y-4">
|
|
<div>
|
|
<label for="email-to" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.emailTo') }}</label>
|
|
<input type="email" id="email-to" v-model="emailConfig.to" required multiple placeholder="recipient1@example.com, recipient2@example.com"
|
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary">
|
|
<small class="block mt-1 text-xs text-text-secondary">{{ $t('settings.notifications.form.emailToHelp') }}</small>
|
|
</div>
|
|
<div>
|
|
<label for="email-body" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.emailBodyTemplate') }}</label> <!-- Changed key -->
|
|
<textarea id="email-body" v-model="emailConfig.bodyTemplate" rows="3" :placeholder="`${$t('settings.notifications.form.emailBodyPlaceholder')} {event}, {timestamp}, {details}`"
|
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary font-mono text-sm"></textarea> <!-- Changed to textarea and v-model -->
|
|
<small class="block mt-1 text-xs text-text-secondary">{{ $t('settings.notifications.form.templateHelp') }} {event}, {timestamp}, {details}</small> <!-- Added available placeholders -->
|
|
</div>
|
|
<!-- SMTP Settings -->
|
|
<div>
|
|
<label for="smtp-host" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.smtpHost') }}</label>
|
|
<input type="text" id="smtp-host" v-model="emailConfig.smtpHost" required
|
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary">
|
|
</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label for="smtp-port" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.smtpPort') }}</label>
|
|
<input type="number" id="smtp-port" v-model.number="emailConfig.smtpPort" required
|
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary">
|
|
</div>
|
|
<div class="flex items-end pb-1"> <!-- Align checkbox with bottom of port input -->
|
|
<div class="flex items-center">
|
|
<input type="checkbox" id="smtp-secure" v-model="emailConfig.smtpSecure"
|
|
class="h-4 w-4 rounded border-border text-primary focus:ring-primary mr-2 cursor-pointer">
|
|
<label for="smtp-secure" class="text-sm text-foreground cursor-pointer">{{ $t('settings.notifications.form.smtpSecure') }} (TLS)</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label for="smtp-user" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.smtpUser') }}</label>
|
|
<input type="text" id="smtp-user" v-model="emailConfig.smtpUser" autocomplete="off"
|
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary">
|
|
</div>
|
|
<div>
|
|
<label for="smtp-pass" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.smtpPass') }}</label>
|
|
<input type="password" id="smtp-pass" v-model="emailConfig.smtpPass" autocomplete="new-password"
|
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary">
|
|
</div>
|
|
<div>
|
|
<label for="smtp-from" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.smtpFrom') }}</label>
|
|
<input type="email" id="smtp-from" v-model="emailConfig.from" required placeholder="sender@example.com"
|
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary">
|
|
<small class="block mt-1 text-xs text-text-secondary">{{ $t('settings.notifications.form.smtpFromHelp') }}</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Telegram Config -->
|
|
<div v-if="formData.channel_type === 'telegram'" class="space-y-4">
|
|
<div>
|
|
<label for="telegram-token" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.telegramToken') }}</label>
|
|
<input type="password" id="telegram-token" v-model="telegramConfig.botToken" required autocomplete="new-password"
|
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary">
|
|
|
|
</div>
|
|
<div>
|
|
<label for="telegram-chatid" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.telegramChatId') }}</label>
|
|
<input type="text" id="telegram-chatid" v-model="telegramConfig.chatId" required
|
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary">
|
|
</div>
|
|
<div>
|
|
<label for="telegram-message" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.telegramMessageTemplate') }}</label>
|
|
<textarea id="telegram-message" v-model="telegramConfig.messageTemplate" rows="3" :placeholder="`${$t('settings.notifications.form.telegramMessagePlaceholder')} {event}, {timestamp}, {details}.`"
|
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary font-mono text-sm"></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Unified Test Button Area -->
|
|
<div class="text-center pt-4 mt-4 border-t border-border/50"> <!-- Test button container -->
|
|
<button
|
|
v-if="isEditing || canTestUnsaved"
|
|
type="button"
|
|
@click="handleTestNotification"
|
|
:disabled="testingNotification"
|
|
class="px-3 py-1.5 border border-border rounded-md text-sm font-medium text-text-secondary bg-background hover:bg-header focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center"
|
|
>
|
|
<svg v-if="testingNotification" class="animate-spin -ml-0.5 mr-2 h-4 w-4 text-text-secondary" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
{{ testingNotification ? $t('common.testing') : $t('settings.notifications.form.testButton') }}
|
|
</button>
|
|
<!-- Show hint if adding and required fields are NOT filled -->
|
|
<small v-else class="block mt-2 text-xs text-text-secondary">
|
|
{{ $t('settings.notifications.form.fillRequiredToTest') }}
|
|
</small>
|
|
<!-- Show test result message if available -->
|
|
<small v-if="testResult" :class="['block mt-2 text-xs', testResult.success ? 'text-success' : 'text-error']"> <!-- Use text-success/text-error -->
|
|
{{ testResult.message }}
|
|
</small>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- Enabled Events -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-text-secondary mb-2">{{ $t('settings.notifications.form.enabledEvents') }}</label>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-x-4 gap-y-2"> <!-- Responsive grid for events -->
|
|
<div v-for="event in allNotificationEvents" :key="event">
|
|
<div class="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
:id="'event-' + event"
|
|
:value="event"
|
|
v-model="formData.enabled_events"
|
|
class="h-4 w-4 rounded border-border text-primary focus:ring-primary mr-2 cursor-pointer"
|
|
>
|
|
<label :for="'event-' + event" class="text-sm text-foreground cursor-pointer select-none">{{ getEventDisplayName(event) }}</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- Form Actions -->
|
|
<div class="flex justify-end space-x-3 pt-5 mt-6 border-t border-border">
|
|
<button type="button" @click="handleCancel"
|
|
class="px-4 py-2 bg-transparent text-text-secondary border border-border rounded-md shadow-sm hover:bg-border hover:text-foreground focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary disabled:opacity-50 disabled:cursor-not-allowed transition duration-150 ease-in-out">
|
|
{{ $t('common.cancel') }}
|
|
</button>
|
|
<button type="submit" :disabled="store.isLoading || !!headerError || testingNotification"
|
|
class="px-4 py-2 bg-button text-button-text rounded-md shadow-sm hover:bg-button-hover focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary disabled:opacity-50 disabled:cursor-not-allowed transition duration-150 ease-in-out">
|
|
{{ store.isLoading ? $t('common.saving') : $t('common.save') }}
|
|
</button>
|
|
</div>
|
|
<div v-if="formError" class="p-3 mt-3 border-l-4 border-error bg-error/10 text-error text-sm rounded">{{ formError }}</div> <!-- Use error colors -->
|
|
<div v-if="testError" class="p-3 mt-3 border-l-4 border-error bg-error/10 text-error text-sm rounded">{{ testError }}</div> <!-- Use error colors -->
|
|
</form>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, reactive, computed, watch, PropType, nextTick } from 'vue';
|
|
import { useNotificationsStore } from '../stores/notifications.store';
|
|
import {
|
|
NotificationSetting,
|
|
NotificationEvent,
|
|
WebhookConfig,
|
|
EmailConfig,
|
|
TelegramConfig
|
|
} from '../types/server.types';
|
|
import { useI18n } from 'vue-i18n';
|
|
|
|
|
|
interface SmtpEmailConfig extends Omit<EmailConfig, 'subjectTemplate'> {
|
|
bodyTemplate?: string;
|
|
smtpHost?: string;
|
|
smtpPort?: number;
|
|
smtpSecure?: boolean;
|
|
smtpUser?: string;
|
|
smtpPass?: string;
|
|
from?: string; // Add 'from' address
|
|
}
|
|
|
|
const props = defineProps({
|
|
initialData: {
|
|
type: Object as PropType<NotificationSetting | null>,
|
|
default: null,
|
|
},
|
|
});
|
|
|
|
const emit = defineEmits(['save', 'cancel']);
|
|
|
|
const store = useNotificationsStore();
|
|
console.log('[NotificationSettingForm] Setup started.'); // Log setup start
|
|
const { t } = useI18n();
|
|
console.log('[NotificationSettingForm] useI18n initialized.'); // Log i18n init
|
|
const formError = ref<string | null>(null);
|
|
const headerError = ref<string | null>(null);
|
|
const testError = ref<string | null>(null);
|
|
const testingNotification = ref(false);
|
|
const testResult = ref<{ success: boolean; message: string } | null>(null);
|
|
|
|
|
|
const isEditing = computed(() => !!props.initialData?.id);
|
|
|
|
// Computed property to check if necessary fields for testing unsaved config are filled
|
|
const canTestUnsaved = computed(() => {
|
|
if (isEditing.value) return true; // Always allow testing saved settings
|
|
|
|
switch (formData.channel_type) {
|
|
case 'webhook':
|
|
return !!webhookConfig.value.url && !headerError.value;
|
|
case 'email':
|
|
return !!emailConfig.value.to && !!emailConfig.value.smtpHost && !!emailConfig.value.smtpPort && !!emailConfig.value.from;
|
|
case 'telegram':
|
|
return !!telegramConfig.value.botToken && !!telegramConfig.value.chatId;
|
|
default:
|
|
return false;
|
|
}
|
|
});
|
|
|
|
|
|
// Define all possible events (aligned with AuditLogView's allActionTypes)
|
|
const allNotificationEvents: NotificationEvent[] = [
|
|
'LOGIN_SUCCESS', 'LOGIN_FAILURE', 'LOGOUT', 'PASSWORD_CHANGED',
|
|
'2FA_ENABLED', '2FA_DISABLED',
|
|
// Passkey Events
|
|
'PASSKEY_AUTH_SUCCESS',
|
|
'PASSKEY_AUTH_FAILURE',
|
|
'CONNECTION_CREATED', 'CONNECTION_UPDATED', 'CONNECTION_DELETED',
|
|
'PROXY_CREATED', 'PROXY_UPDATED', 'PROXY_DELETED',
|
|
'TAG_CREATED', 'TAG_UPDATED', 'TAG_DELETED',
|
|
'SETTINGS_UPDATED', 'IP_WHITELIST_UPDATED', 'IP_BLOCKED', // Added IP_BLOCKED as it's in backend types
|
|
'NOTIFICATION_SETTING_CREATED', 'NOTIFICATION_SETTING_UPDATED', 'NOTIFICATION_SETTING_DELETED',
|
|
'SSH_CONNECT_SUCCESS', 'SSH_CONNECT_FAILURE', 'SSH_SHELL_FAILURE',
|
|
'DATABASE_MIGRATION', 'ADMIN_SETUP_COMPLETE'
|
|
// Removed IP_BLACKLISTED as it's not in the Audit Log list source, but IP_BLOCKED is present in backend types
|
|
];
|
|
|
|
// Reactive form data structure
|
|
const getDefaultFormData = (): Omit<NotificationSetting, 'id' | 'created_at' | 'updated_at' | 'config'> & { config: any } => ({
|
|
name: '',
|
|
enabled: true,
|
|
channel_type: 'webhook',
|
|
config: {}, // Will be populated based on channel_type
|
|
enabled_events: ['LOGIN_FAILURE'], // Sensible defaults
|
|
});
|
|
|
|
const formData = reactive(getDefaultFormData());
|
|
|
|
// Specific config refs for easier v-model binding
|
|
const webhookConfig = ref<WebhookConfig>({ url: '', method: 'POST', headers: {}, bodyTemplate: '' });
|
|
const emailConfig = ref<SmtpEmailConfig>({ // Use extended type
|
|
to: '',
|
|
bodyTemplate: '', // Changed from subjectTemplate
|
|
smtpHost: '',
|
|
smtpPort: 587, // Default port
|
|
smtpSecure: true, // Default to true (TLS)
|
|
smtpUser: '',
|
|
smtpPass: '',
|
|
from: ''
|
|
});
|
|
const telegramConfig = ref<TelegramConfig>({ botToken: '', chatId: '', messageTemplate: '' });
|
|
const webhookHeadersString = ref('{}'); // For textarea binding
|
|
|
|
// Watch for initialData changes (when editing)
|
|
watch(() => props.initialData, (newData) => {
|
|
console.log('[NotificationSettingForm] Watch initialData triggered. New data:', newData); // Log initialData change
|
|
if (newData) {
|
|
Object.assign(formData, newData);
|
|
// Populate specific config refs based on channel type
|
|
if (newData.channel_type === 'webhook') {
|
|
webhookConfig.value = { ...(newData.config as WebhookConfig) };
|
|
webhookHeadersString.value = JSON.stringify(webhookConfig.value.headers || {}, null, 2);
|
|
} else if (newData.channel_type === 'email') {
|
|
// Ensure all fields are present, providing defaults if missing from saved config
|
|
const savedConfig = newData.config as SmtpEmailConfig;
|
|
emailConfig.value = {
|
|
to: savedConfig.to || '',
|
|
bodyTemplate: savedConfig.bodyTemplate || '', // Changed from subjectTemplate
|
|
smtpHost: savedConfig.smtpHost || '',
|
|
smtpPort: savedConfig.smtpPort || 587,
|
|
smtpSecure: savedConfig.smtpSecure === undefined ? true : savedConfig.smtpSecure, // Default true if undefined
|
|
smtpUser: savedConfig.smtpUser || '',
|
|
smtpPass: savedConfig.smtpPass || '', // Password might not be sent back, handle appropriately
|
|
from: savedConfig.from || ''
|
|
};
|
|
} else if (newData.channel_type === 'telegram') {
|
|
telegramConfig.value = { ...(newData.config as TelegramConfig) };
|
|
}
|
|
} else {
|
|
// Reset form if initialData becomes null (e.g., switching from edit to add)
|
|
Object.assign(formData, getDefaultFormData());
|
|
webhookConfig.value = { url: '', method: 'POST', headers: {}, bodyTemplate: '' };
|
|
// Reset email config with defaults
|
|
emailConfig.value = {
|
|
to: '', bodyTemplate: '', smtpHost: '', smtpPort: 587, smtpSecure: true, smtpUser: '', smtpPass: '', from: '' // Changed from subjectTemplate
|
|
};
|
|
telegramConfig.value = { botToken: '', chatId: '', messageTemplate: '' };
|
|
webhookHeadersString.value = '{}';
|
|
}
|
|
headerError.value = null; // Reset header error on data change
|
|
testError.value = null; // Reset test error
|
|
testResult.value = null; // Reset test result
|
|
testingNotification.value = false; // Reset testing state
|
|
console.log('[NotificationSettingForm] Form data initialized/updated from initialData. Current channel_type:', formData.channel_type); // Log after init/update
|
|
}, { immediate: true });
|
|
|
|
// Watch channel type change to reset specific config
|
|
watch(() => formData.channel_type, (newType, oldType) => {
|
|
console.log(`[NotificationSettingForm] Watch channel_type changed from ${oldType} to ${newType}`); // Log channel type change
|
|
if (newType !== oldType && !isEditing.value) { // Only reset if not editing or type changes during add mode
|
|
webhookConfig.value = { url: '', method: 'POST', headers: {}, bodyTemplate: '' };
|
|
emailConfig.value = {
|
|
to: '', bodyTemplate: '', smtpHost: '', smtpPort: 587, smtpSecure: true, smtpUser: '', smtpPass: '', from: '' // Changed from subjectTemplate
|
|
};
|
|
telegramConfig.value = { botToken: '', chatId: '', messageTemplate: '' };
|
|
webhookHeadersString.value = '{}';
|
|
headerError.value = null;
|
|
testError.value = null;
|
|
testResult.value = null;
|
|
testingNotification.value = false;
|
|
}
|
|
// Always reset test state when type changes
|
|
testError.value = null;
|
|
testResult.value = null;
|
|
testingNotification.value = false;
|
|
});
|
|
|
|
// Watch header string to validate JSON
|
|
watch(webhookHeadersString, (newVal) => {
|
|
if (formData.channel_type !== 'webhook') return;
|
|
try {
|
|
const parsed = JSON.parse(newVal || '{}');
|
|
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
throw new Error('Headers must be a JSON object.');
|
|
}
|
|
webhookConfig.value.headers = parsed;
|
|
headerError.value = null;
|
|
} catch (e: any) {
|
|
headerError.value = t('settings.notifications.form.invalidJson') + `: ${e.message}`;
|
|
}
|
|
});
|
|
|
|
// Watch for changes in email config to clear previous test results
|
|
watch(emailConfig, () => {
|
|
testResult.value = null;
|
|
testError.value = null;
|
|
}, { deep: true });
|
|
|
|
|
|
const getEventDisplayName = (event: NotificationEvent): string => {
|
|
// Use i18n key, fallback to formatted name if key not found
|
|
const i18nKey = `settings.notifications.events.${event}`;
|
|
console.log(`[NotificationSettingForm] Translating event display name for key: ${i18nKey}`); // Log event key translation attempt
|
|
const translated = t(i18nKey);
|
|
// If translation returns the key itself, it means translation is missing
|
|
if (translated === i18nKey) {
|
|
console.warn(`Missing translation for notification event: ${i18nKey}`);
|
|
return event.replace(/_/g, ' ').toLowerCase().replace(/\b\w/g, l => l.toUpperCase()); // Fallback
|
|
}
|
|
return translated;
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
console.log('[NotificationSettingForm] handleSubmit called.'); // Log submit start
|
|
formError.value = null;
|
|
if (headerError.value) return; // Don't submit if headers are invalid
|
|
|
|
// Assign the correct config based on channel type
|
|
if (formData.channel_type === 'webhook') {
|
|
formData.config = webhookConfig.value;
|
|
} else if (formData.channel_type === 'email') {
|
|
formData.config = emailConfig.value;
|
|
} else if (formData.channel_type === 'telegram') {
|
|
formData.config = telegramConfig.value;
|
|
}
|
|
|
|
let result: NotificationSetting | null = null;
|
|
if (isEditing.value && props.initialData?.id) {
|
|
result = await store.updateSetting(props.initialData.id, formData);
|
|
} else {
|
|
result = await store.addSetting(formData);
|
|
}
|
|
|
|
if (result) {
|
|
emit('save', result);
|
|
} else {
|
|
formError.value = store.error || t('common.errorOccurred');
|
|
}
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
emit('cancel');
|
|
};
|
|
|
|
const handleTestNotification = async () => {
|
|
// Allow testing if editing OR if adding and required fields are filled
|
|
if (!isEditing.value && !canTestUnsaved.value) return;
|
|
|
|
testingNotification.value = true;
|
|
testError.value = null;
|
|
testResult.value = null;
|
|
|
|
let testConfig: any = {};
|
|
// Prepare the config based on the current channel type
|
|
switch (formData.channel_type) {
|
|
case 'webhook':
|
|
testConfig = { ...webhookConfig.value };
|
|
// Ensure headers are parsed correctly before sending
|
|
try {
|
|
testConfig.headers = JSON.parse(webhookHeadersString.value || '{}');
|
|
if (typeof testConfig.headers !== 'object' || testConfig.headers === null || Array.isArray(testConfig.headers)) {
|
|
throw new Error('Headers must be a JSON object.');
|
|
}
|
|
} catch (e: any) {
|
|
testResult.value = { success: false, message: t('settings.notifications.form.invalidJson') + `: ${e.message}` };
|
|
testingNotification.value = false;
|
|
return;
|
|
}
|
|
break;
|
|
case 'email':
|
|
testConfig = { ...emailConfig.value };
|
|
break;
|
|
case 'telegram':
|
|
testConfig = { ...telegramConfig.value };
|
|
break;
|
|
default:
|
|
console.error("Unknown channel type for testing:", formData.channel_type);
|
|
testResult.value = { success: false, message: "未知渠道类型无法测试" };
|
|
testingNotification.value = false;
|
|
return;
|
|
}
|
|
|
|
try {
|
|
let result: { success: boolean; message: string };
|
|
if (isEditing.value && props.initialData?.id) {
|
|
// Test existing setting
|
|
result = await store.testSetting(props.initialData.id, testConfig);
|
|
} else {
|
|
// Test unsaved setting
|
|
result = await store.testUnsavedSetting(formData.channel_type, testConfig);
|
|
}
|
|
// Translate the message received from the backend using t()
|
|
testResult.value = { success: true, message: t(result.message || 'settings.notifications.form.testSuccess') };
|
|
} catch (error: any) {
|
|
console.error("Test notification error:", error);
|
|
const message = error?.response?.data?.message || error.message || t('settings.notifications.form.testFailed');
|
|
testResult.value = { success: false, message: message };
|
|
// Optionally set testError if you want a separate display area for errors vs results
|
|
// testError.value = message;
|
|
} finally {
|
|
testingNotification.value = false;
|
|
// Automatically clear the result message after a few seconds
|
|
await nextTick(); // Ensure DOM is updated before setting timeout
|
|
setTimeout(() => {
|
|
testResult.value = null;
|
|
}, 5000); // Clear after 5 seconds
|
|
}
|
|
};
|
|
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Remove all scoped styles as they are now handled by Tailwind utility classes */
|
|
</style>
|