mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-06 16:31:12 +08:00
Add comprehensive tests for ast-grep and tree-sitter relationship extraction
- Introduced test suite for AstGrepPythonProcessor covering pattern definitions, parsing, and relationship extraction. - Added comparison tests between tree-sitter and ast-grep for consistency in relationship extraction. - Implemented tests for ast-grep binding module to verify functionality and availability. - Ensured tests cover various scenarios including inheritance, function calls, and imports.
This commit is contained in:
396
ccw/frontend/src/components/settings/PlatformConfigCards.tsx
Normal file
396
ccw/frontend/src/components/settings/PlatformConfigCards.tsx
Normal file
@@ -0,0 +1,396 @@
|
||||
// ========================================
|
||||
// Platform Configuration Cards
|
||||
// ========================================
|
||||
// Individual configuration cards for each notification platform
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import {
|
||||
MessageCircle,
|
||||
Send,
|
||||
Link,
|
||||
Check,
|
||||
X,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
TestTube,
|
||||
Eye,
|
||||
EyeOff,
|
||||
} from 'lucide-react';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type {
|
||||
RemoteNotificationConfig,
|
||||
NotificationPlatform,
|
||||
DiscordConfig,
|
||||
TelegramConfig,
|
||||
WebhookConfig,
|
||||
} from '@/types/remote-notification';
|
||||
import { PLATFORM_INFO } from '@/types/remote-notification';
|
||||
|
||||
interface PlatformConfigCardsProps {
|
||||
config: RemoteNotificationConfig;
|
||||
expandedPlatform: NotificationPlatform | null;
|
||||
testing: NotificationPlatform | null;
|
||||
onToggleExpand: (platform: NotificationPlatform | null) => void;
|
||||
onUpdateConfig: (
|
||||
platform: NotificationPlatform,
|
||||
updates: Partial<DiscordConfig | TelegramConfig | WebhookConfig>
|
||||
) => void;
|
||||
onTest: (
|
||||
platform: NotificationPlatform,
|
||||
config: DiscordConfig | TelegramConfig | WebhookConfig
|
||||
) => void;
|
||||
onSave: () => void;
|
||||
saving: boolean;
|
||||
}
|
||||
|
||||
export function PlatformConfigCards({
|
||||
config,
|
||||
expandedPlatform,
|
||||
testing,
|
||||
onToggleExpand,
|
||||
onUpdateConfig,
|
||||
onTest,
|
||||
onSave,
|
||||
saving,
|
||||
}: PlatformConfigCardsProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const platforms: NotificationPlatform[] = ['discord', 'telegram', 'webhook'];
|
||||
|
||||
const getPlatformIcon = (platform: NotificationPlatform) => {
|
||||
switch (platform) {
|
||||
case 'discord':
|
||||
return <MessageCircle className="w-4 h-4" />;
|
||||
case 'telegram':
|
||||
return <Send className="w-4 h-4" />;
|
||||
case 'webhook':
|
||||
return <Link className="w-4 h-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getPlatformConfig = (
|
||||
platform: NotificationPlatform
|
||||
): DiscordConfig | TelegramConfig | WebhookConfig => {
|
||||
switch (platform) {
|
||||
case 'discord':
|
||||
return config.platforms.discord || { enabled: false, webhookUrl: '' };
|
||||
case 'telegram':
|
||||
return config.platforms.telegram || { enabled: false, botToken: '', chatId: '' };
|
||||
case 'webhook':
|
||||
return config.platforms.webhook || { enabled: false, url: '', method: 'POST' };
|
||||
}
|
||||
};
|
||||
|
||||
const isConfigured = (platform: NotificationPlatform): boolean => {
|
||||
const platformConfig = getPlatformConfig(platform);
|
||||
switch (platform) {
|
||||
case 'discord':
|
||||
return !!(platformConfig as DiscordConfig).webhookUrl;
|
||||
case 'telegram':
|
||||
return !!(platformConfig as TelegramConfig).botToken && !!(platformConfig as TelegramConfig).chatId;
|
||||
case 'webhook':
|
||||
return !!(platformConfig as WebhookConfig).url;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid gap-3">
|
||||
{platforms.map((platform) => {
|
||||
const info = PLATFORM_INFO[platform];
|
||||
const platformConfig = getPlatformConfig(platform);
|
||||
const configured = isConfigured(platform);
|
||||
const expanded = expandedPlatform === platform;
|
||||
|
||||
return (
|
||||
<Card key={platform} className="overflow-hidden">
|
||||
{/* Header */}
|
||||
<div
|
||||
className="p-4 cursor-pointer hover:bg-muted/50 transition-colors"
|
||||
onClick={() => onToggleExpand(expanded ? null : platform)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={cn(
|
||||
'p-2 rounded-lg',
|
||||
platformConfig.enabled && configured
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'bg-muted text-muted-foreground'
|
||||
)}>
|
||||
{getPlatformIcon(platform)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">{info.name}</span>
|
||||
{configured && (
|
||||
<Badge variant="outline" className="text-xs text-green-600 border-green-500/30">
|
||||
<Check className="w-3 h-3 mr-1" />
|
||||
{formatMessage({ id: 'settings.remoteNotifications.configured' })}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">{info.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant={platformConfig.enabled ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
className="h-7"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUpdateConfig(platform, { enabled: !platformConfig.enabled });
|
||||
}}
|
||||
>
|
||||
{platformConfig.enabled ? (
|
||||
<Check className="w-3.5 h-3.5" />
|
||||
) : (
|
||||
<X className="w-3.5 h-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
{expanded ? (
|
||||
<ChevronUp className="w-4 h-4 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronDown className="w-4 h-4 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded Content */}
|
||||
{expanded && (
|
||||
<div className="border-t border-border p-4 space-y-4 bg-muted/30">
|
||||
{platform === 'discord' && (
|
||||
<DiscordConfigForm
|
||||
config={platformConfig as DiscordConfig}
|
||||
onUpdate={(updates) => onUpdateConfig('discord', updates)}
|
||||
/>
|
||||
)}
|
||||
{platform === 'telegram' && (
|
||||
<TelegramConfigForm
|
||||
config={platformConfig as TelegramConfig}
|
||||
onUpdate={(updates) => onUpdateConfig('telegram', updates)}
|
||||
/>
|
||||
)}
|
||||
{platform === 'webhook' && (
|
||||
<WebhookConfigForm
|
||||
config={platformConfig as WebhookConfig}
|
||||
onUpdate={(updates) => onUpdateConfig('webhook', updates)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center gap-2 pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onTest(platform, platformConfig)}
|
||||
disabled={testing === platform || !configured}
|
||||
>
|
||||
<TestTube className={cn('w-3.5 h-3.5 mr-1', testing === platform && 'animate-pulse')} />
|
||||
{formatMessage({ id: 'settings.remoteNotifications.testConnection' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={onSave}
|
||||
disabled={saving}
|
||||
>
|
||||
{formatMessage({ id: 'settings.remoteNotifications.save' })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Discord Config Form ==========
|
||||
|
||||
function DiscordConfigForm({
|
||||
config,
|
||||
onUpdate,
|
||||
}: {
|
||||
config: DiscordConfig;
|
||||
onUpdate: (updates: Partial<DiscordConfig>) => void;
|
||||
}) {
|
||||
const { formatMessage } = useIntl();
|
||||
const [showUrl, setShowUrl] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.discord.webhookUrl' })}
|
||||
</label>
|
||||
<div className="flex gap-2 mt-1">
|
||||
<Input
|
||||
type={showUrl ? 'text' : 'password'}
|
||||
value={config.webhookUrl || ''}
|
||||
onChange={(e) => onUpdate({ webhookUrl: e.target.value })}
|
||||
placeholder="https://discord.com/api/webhooks/..."
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="shrink-0"
|
||||
onClick={() => setShowUrl(!showUrl)}
|
||||
>
|
||||
{showUrl ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.discord.webhookUrlHint' })}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.discord.username' })}
|
||||
</label>
|
||||
<Input
|
||||
value={config.username || ''}
|
||||
onChange={(e) => onUpdate({ username: e.target.value })}
|
||||
placeholder="CCW Notification"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Telegram Config Form ==========
|
||||
|
||||
function TelegramConfigForm({
|
||||
config,
|
||||
onUpdate,
|
||||
}: {
|
||||
config: TelegramConfig;
|
||||
onUpdate: (updates: Partial<TelegramConfig>) => void;
|
||||
}) {
|
||||
const { formatMessage } = useIntl();
|
||||
const [showToken, setShowToken] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.telegram.botToken' })}
|
||||
</label>
|
||||
<div className="flex gap-2 mt-1">
|
||||
<Input
|
||||
type={showToken ? 'text' : 'password'}
|
||||
value={config.botToken || ''}
|
||||
onChange={(e) => onUpdate({ botToken: e.target.value })}
|
||||
placeholder="1234567890:ABCdefGHIjklMNOpqrsTUVwxyz"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="shrink-0"
|
||||
onClick={() => setShowToken(!showToken)}
|
||||
>
|
||||
{showToken ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.telegram.botTokenHint' })}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.telegram.chatId' })}
|
||||
</label>
|
||||
<Input
|
||||
value={config.chatId || ''}
|
||||
onChange={(e) => onUpdate({ chatId: e.target.value })}
|
||||
placeholder="-1001234567890"
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.telegram.chatIdHint' })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Webhook Config Form ==========
|
||||
|
||||
function WebhookConfigForm({
|
||||
config,
|
||||
onUpdate,
|
||||
}: {
|
||||
config: WebhookConfig;
|
||||
onUpdate: (updates: Partial<WebhookConfig>) => void;
|
||||
}) {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.webhook.url' })}
|
||||
</label>
|
||||
<Input
|
||||
value={config.url || ''}
|
||||
onChange={(e) => onUpdate({ url: e.target.value })}
|
||||
placeholder="https://your-server.com/webhook"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.webhook.method' })}
|
||||
</label>
|
||||
<div className="flex gap-2 mt-1">
|
||||
<Button
|
||||
variant={config.method === 'POST' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => onUpdate({ method: 'POST' })}
|
||||
>
|
||||
POST
|
||||
</Button>
|
||||
<Button
|
||||
variant={config.method === 'PUT' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => onUpdate({ method: 'PUT' })}
|
||||
>
|
||||
PUT
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.webhook.headers' })}
|
||||
</label>
|
||||
<Input
|
||||
value={config.headers ? JSON.stringify(config.headers) : ''}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
const headers = e.target.value ? JSON.parse(e.target.value) : undefined;
|
||||
onUpdate({ headers });
|
||||
} catch {
|
||||
// Invalid JSON, ignore
|
||||
}
|
||||
}}
|
||||
placeholder='{"Authorization": "Bearer token"}'
|
||||
className="mt-1 font-mono text-xs"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.webhook.headersHint' })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PlatformConfigCards;
|
||||
@@ -0,0 +1,347 @@
|
||||
// ========================================
|
||||
// Remote Notification Settings Section
|
||||
// ========================================
|
||||
// Configuration UI for remote notification platforms
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import {
|
||||
Bell,
|
||||
BellOff,
|
||||
RefreshCw,
|
||||
Check,
|
||||
X,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
TestTube,
|
||||
Save,
|
||||
AlertTriangle,
|
||||
} from 'lucide-react';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { toast } from 'sonner';
|
||||
import type {
|
||||
RemoteNotificationConfig,
|
||||
NotificationPlatform,
|
||||
EventConfig,
|
||||
DiscordConfig,
|
||||
TelegramConfig,
|
||||
WebhookConfig,
|
||||
} from '@/types/remote-notification';
|
||||
import { PLATFORM_INFO, EVENT_INFO, getDefaultConfig } from '@/types/remote-notification';
|
||||
import { PlatformConfigCards } from './PlatformConfigCards';
|
||||
|
||||
interface RemoteNotificationSectionProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function RemoteNotificationSection({ className }: RemoteNotificationSectionProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
const [config, setConfig] = useState<RemoteNotificationConfig | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [testing, setTesting] = useState<NotificationPlatform | null>(null);
|
||||
const [expandedPlatform, setExpandedPlatform] = useState<NotificationPlatform | null>(null);
|
||||
|
||||
// Load configuration
|
||||
const loadConfig = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch('/api/notifications/remote/config');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setConfig(data);
|
||||
} else {
|
||||
// Use default config if not found
|
||||
setConfig(getDefaultConfig());
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load remote notification config:', error);
|
||||
setConfig(getDefaultConfig());
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadConfig();
|
||||
}, [loadConfig]);
|
||||
|
||||
// Save configuration
|
||||
const saveConfig = useCallback(async (newConfig: RemoteNotificationConfig) => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const response = await fetch('/api/notifications/remote/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(newConfig),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setConfig(data.config);
|
||||
toast.success(formatMessage({ id: 'settings.remoteNotifications.saved' }));
|
||||
} else {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(formatMessage({ id: 'settings.remoteNotifications.saveError' }));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [formatMessage]);
|
||||
|
||||
// Test platform
|
||||
const testPlatform = useCallback(async (
|
||||
platform: NotificationPlatform,
|
||||
platformConfig: DiscordConfig | TelegramConfig | WebhookConfig
|
||||
) => {
|
||||
setTesting(platform);
|
||||
try {
|
||||
const response = await fetch('/api/notifications/remote/test', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ platform, config: platformConfig }),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
toast.success(
|
||||
formatMessage({ id: 'settings.remoteNotifications.testSuccess' }),
|
||||
{ description: `${result.responseTime}ms` }
|
||||
);
|
||||
} else {
|
||||
toast.error(
|
||||
formatMessage({ id: 'settings.remoteNotifications.testFailed' }),
|
||||
{ description: result.error }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(formatMessage({ id: 'settings.remoteNotifications.testError' }));
|
||||
} finally {
|
||||
setTesting(null);
|
||||
}
|
||||
}, [formatMessage]);
|
||||
|
||||
// Toggle master switch
|
||||
const toggleEnabled = () => {
|
||||
if (!config) return;
|
||||
saveConfig({ ...config, enabled: !config.enabled });
|
||||
};
|
||||
|
||||
// Update platform config
|
||||
const updatePlatformConfig = (
|
||||
platform: NotificationPlatform,
|
||||
updates: Partial<DiscordConfig | TelegramConfig | WebhookConfig>
|
||||
) => {
|
||||
if (!config) return;
|
||||
const newConfig = {
|
||||
...config,
|
||||
platforms: {
|
||||
...config.platforms,
|
||||
[platform]: {
|
||||
...config.platforms[platform as keyof typeof config.platforms],
|
||||
...updates,
|
||||
},
|
||||
},
|
||||
};
|
||||
setConfig(newConfig);
|
||||
};
|
||||
|
||||
// Update event config
|
||||
const updateEventConfig = (eventIndex: number, updates: Partial<EventConfig>) => {
|
||||
if (!config) return;
|
||||
const newEvents = [...config.events];
|
||||
newEvents[eventIndex] = { ...newEvents[eventIndex], ...updates };
|
||||
setConfig({ ...config, events: newEvents });
|
||||
};
|
||||
|
||||
// Reset to defaults
|
||||
const resetConfig = async () => {
|
||||
if (!confirm(formatMessage({ id: 'settings.remoteNotifications.resetConfirm' }))) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch('/api/notifications/remote/reset', {
|
||||
method: 'POST',
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setConfig(data.config);
|
||||
toast.success(formatMessage({ id: 'settings.remoteNotifications.resetSuccess' }));
|
||||
}
|
||||
} catch {
|
||||
toast.error(formatMessage({ id: 'settings.remoteNotifications.resetError' }));
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card className={cn('p-6', className)}>
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<RefreshCw className="w-5 h-5 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className={cn('p-6', className)}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-lg font-semibold text-foreground flex items-center gap-2">
|
||||
{config.enabled ? (
|
||||
<Bell className="w-5 h-5 text-primary" />
|
||||
) : (
|
||||
<BellOff className="w-5 h-5 text-muted-foreground" />
|
||||
)}
|
||||
{formatMessage({ id: 'settings.remoteNotifications.title' })}
|
||||
</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => loadConfig()}
|
||||
disabled={loading}
|
||||
>
|
||||
<RefreshCw className={cn('w-3.5 h-3.5', loading && 'animate-spin')} />
|
||||
</Button>
|
||||
<Button
|
||||
variant={config.enabled ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={toggleEnabled}
|
||||
>
|
||||
{config.enabled ? (
|
||||
<>
|
||||
<Check className="w-4 h-4 mr-1" />
|
||||
{formatMessage({ id: 'settings.remoteNotifications.enabled' })}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<X className="w-4 h-4 mr-1" />
|
||||
{formatMessage({ id: 'settings.remoteNotifications.disabled' })}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-sm text-muted-foreground mb-6">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.description' })}
|
||||
</p>
|
||||
|
||||
{config.enabled && (
|
||||
<>
|
||||
{/* Platform Configuration */}
|
||||
<div className="space-y-4 mb-6">
|
||||
<h3 className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.platforms' })}
|
||||
</h3>
|
||||
<PlatformConfigCards
|
||||
config={config}
|
||||
expandedPlatform={expandedPlatform}
|
||||
testing={testing}
|
||||
onToggleExpand={setExpandedPlatform}
|
||||
onUpdateConfig={updatePlatformConfig}
|
||||
onTest={testPlatform}
|
||||
onSave={() => saveConfig(config)}
|
||||
saving={saving}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Event Configuration */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.events' })}
|
||||
</h3>
|
||||
<div className="grid gap-3">
|
||||
{config.events.map((eventConfig, index) => {
|
||||
const info = EVENT_INFO[eventConfig.event];
|
||||
return (
|
||||
<div
|
||||
key={eventConfig.event}
|
||||
className="flex items-center justify-between p-3 rounded-lg border border-border bg-muted/30"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={cn(
|
||||
'p-2 rounded-lg',
|
||||
eventConfig.enabled ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'
|
||||
)}>
|
||||
<span className="text-sm">{info.icon}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">{info.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{info.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Platform badges */}
|
||||
<div className="flex gap-1">
|
||||
{eventConfig.platforms.map((platform) => (
|
||||
<Badge key={platform} variant="secondary" className="text-xs">
|
||||
{PLATFORM_INFO[platform].name}
|
||||
</Badge>
|
||||
))}
|
||||
{eventConfig.platforms.length === 0 && (
|
||||
<Badge variant="outline" className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.noPlatforms' })}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{/* Toggle */}
|
||||
<Button
|
||||
variant={eventConfig.enabled ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
className="h-7"
|
||||
onClick={() => updateEventConfig(index, { enabled: !eventConfig.enabled })}
|
||||
>
|
||||
{eventConfig.enabled ? (
|
||||
<Check className="w-3.5 h-3.5" />
|
||||
) : (
|
||||
<X className="w-3.5 h-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center justify-between mt-6 pt-4 border-t border-border">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={resetConfig}
|
||||
>
|
||||
{formatMessage({ id: 'settings.remoteNotifications.reset' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={() => saveConfig(config)}
|
||||
disabled={saving}
|
||||
>
|
||||
<Save className="w-4 h-4 mr-1" />
|
||||
{saving
|
||||
? formatMessage({ id: 'settings.remoteNotifications.saving' })
|
||||
: formatMessage({ id: 'settings.remoteNotifications.save' })}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default RemoteNotificationSection;
|
||||
Reference in New Issue
Block a user