mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-06 16:31:12 +08:00
feat: add experimental support for AST parsing and static graph indexing
- Introduced CLI options for using AST grep parsers and enabling static graph relationships during indexing. - Updated configuration management to load new settings for AST parsing and static graph types. - Enhanced AST grep processor to handle imports with aliases and improve relationship tracking. - Modified TreeSitter parsers to support synthetic module scopes for better static graph persistence. - Implemented global relationship updates in the incremental indexer for static graph expansion. - Added new ArtifactTag and FloatingFileBrowser components to the frontend for improved terminal dashboard functionality. - Created utility functions for detecting CCW artifacts in terminal output with associated tests.
This commit is contained in:
@@ -16,6 +16,10 @@ import {
|
||||
TestTube,
|
||||
Eye,
|
||||
EyeOff,
|
||||
MessageSquare,
|
||||
Bell,
|
||||
Users,
|
||||
Mail,
|
||||
} from 'lucide-react';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
@@ -28,6 +32,10 @@ import type {
|
||||
DiscordConfig,
|
||||
TelegramConfig,
|
||||
WebhookConfig,
|
||||
FeishuConfig,
|
||||
DingTalkConfig,
|
||||
WeComConfig,
|
||||
EmailConfig,
|
||||
} from '@/types/remote-notification';
|
||||
import { PLATFORM_INFO } from '@/types/remote-notification';
|
||||
|
||||
@@ -38,11 +46,11 @@ interface PlatformConfigCardsProps {
|
||||
onToggleExpand: (platform: NotificationPlatform | null) => void;
|
||||
onUpdateConfig: (
|
||||
platform: NotificationPlatform,
|
||||
updates: Partial<DiscordConfig | TelegramConfig | WebhookConfig>
|
||||
updates: Partial<DiscordConfig | TelegramConfig | WebhookConfig | FeishuConfig | DingTalkConfig | WeComConfig | EmailConfig>
|
||||
) => void;
|
||||
onTest: (
|
||||
platform: NotificationPlatform,
|
||||
config: DiscordConfig | TelegramConfig | WebhookConfig
|
||||
config: DiscordConfig | TelegramConfig | WebhookConfig | FeishuConfig | DingTalkConfig | WeComConfig | EmailConfig
|
||||
) => void;
|
||||
onSave: () => void;
|
||||
saving: boolean;
|
||||
@@ -60,7 +68,7 @@ export function PlatformConfigCards({
|
||||
}: PlatformConfigCardsProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const platforms: NotificationPlatform[] = ['discord', 'telegram', 'webhook'];
|
||||
const platforms: NotificationPlatform[] = ['discord', 'telegram', 'feishu', 'dingtalk', 'wecom', 'email', 'webhook'];
|
||||
|
||||
const getPlatformIcon = (platform: NotificationPlatform) => {
|
||||
switch (platform) {
|
||||
@@ -68,6 +76,14 @@ export function PlatformConfigCards({
|
||||
return <MessageCircle className="w-4 h-4" />;
|
||||
case 'telegram':
|
||||
return <Send className="w-4 h-4" />;
|
||||
case 'feishu':
|
||||
return <MessageSquare className="w-4 h-4" />;
|
||||
case 'dingtalk':
|
||||
return <Bell className="w-4 h-4" />;
|
||||
case 'wecom':
|
||||
return <Users className="w-4 h-4" />;
|
||||
case 'email':
|
||||
return <Mail className="w-4 h-4" />;
|
||||
case 'webhook':
|
||||
return <Link className="w-4 h-4" />;
|
||||
}
|
||||
@@ -75,12 +91,20 @@ export function PlatformConfigCards({
|
||||
|
||||
const getPlatformConfig = (
|
||||
platform: NotificationPlatform
|
||||
): DiscordConfig | TelegramConfig | WebhookConfig => {
|
||||
): DiscordConfig | TelegramConfig | WebhookConfig | FeishuConfig | DingTalkConfig | WeComConfig | EmailConfig => {
|
||||
switch (platform) {
|
||||
case 'discord':
|
||||
return config.platforms.discord || { enabled: false, webhookUrl: '' };
|
||||
case 'telegram':
|
||||
return config.platforms.telegram || { enabled: false, botToken: '', chatId: '' };
|
||||
case 'feishu':
|
||||
return config.platforms.feishu || { enabled: false, webhookUrl: '' };
|
||||
case 'dingtalk':
|
||||
return config.platforms.dingtalk || { enabled: false, webhookUrl: '' };
|
||||
case 'wecom':
|
||||
return config.platforms.wecom || { enabled: false, webhookUrl: '' };
|
||||
case 'email':
|
||||
return config.platforms.email || { enabled: false, host: '', port: 587, username: '', password: '', from: '', to: [] };
|
||||
case 'webhook':
|
||||
return config.platforms.webhook || { enabled: false, url: '', method: 'POST' };
|
||||
}
|
||||
@@ -93,6 +117,15 @@ export function PlatformConfigCards({
|
||||
return !!(platformConfig as DiscordConfig).webhookUrl;
|
||||
case 'telegram':
|
||||
return !!(platformConfig as TelegramConfig).botToken && !!(platformConfig as TelegramConfig).chatId;
|
||||
case 'feishu':
|
||||
return !!(platformConfig as FeishuConfig).webhookUrl;
|
||||
case 'dingtalk':
|
||||
return !!(platformConfig as DingTalkConfig).webhookUrl;
|
||||
case 'wecom':
|
||||
return !!(platformConfig as WeComConfig).webhookUrl;
|
||||
case 'email':
|
||||
const emailConfig = platformConfig as EmailConfig;
|
||||
return !!(emailConfig.host && emailConfig.username && emailConfig.password && emailConfig.from && emailConfig.to?.length > 0);
|
||||
case 'webhook':
|
||||
return !!(platformConfig as WebhookConfig).url;
|
||||
}
|
||||
@@ -176,6 +209,30 @@ export function PlatformConfigCards({
|
||||
onUpdate={(updates) => onUpdateConfig('telegram', updates)}
|
||||
/>
|
||||
)}
|
||||
{platform === 'feishu' && (
|
||||
<FeishuConfigForm
|
||||
config={platformConfig as FeishuConfig}
|
||||
onUpdate={(updates) => onUpdateConfig('feishu', updates)}
|
||||
/>
|
||||
)}
|
||||
{platform === 'dingtalk' && (
|
||||
<DingTalkConfigForm
|
||||
config={platformConfig as DingTalkConfig}
|
||||
onUpdate={(updates) => onUpdateConfig('dingtalk', updates)}
|
||||
/>
|
||||
)}
|
||||
{platform === 'wecom' && (
|
||||
<WeComConfigForm
|
||||
config={platformConfig as WeComConfig}
|
||||
onUpdate={(updates) => onUpdateConfig('wecom', updates)}
|
||||
/>
|
||||
)}
|
||||
{platform === 'email' && (
|
||||
<EmailConfigForm
|
||||
config={platformConfig as EmailConfig}
|
||||
onUpdate={(updates) => onUpdateConfig('email', updates)}
|
||||
/>
|
||||
)}
|
||||
{platform === 'webhook' && (
|
||||
<WebhookConfigForm
|
||||
config={platformConfig as WebhookConfig}
|
||||
@@ -393,4 +450,303 @@ function WebhookConfigForm({
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Feishu Config Form ==========
|
||||
|
||||
function FeishuConfigForm({
|
||||
config,
|
||||
onUpdate,
|
||||
}: {
|
||||
config: FeishuConfig;
|
||||
onUpdate: (updates: Partial<FeishuConfig>) => 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.feishu.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://open.feishu.cn/open-apis/bot/v2/hook/..."
|
||||
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.feishu.webhookUrlHint' })}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="feishu-useCard"
|
||||
checked={config.useCard || false}
|
||||
onChange={(e) => onUpdate({ useCard: e.target.checked })}
|
||||
className="rounded border-border"
|
||||
/>
|
||||
<label htmlFor="feishu-useCard" className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.feishu.useCard' })}
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground -mt-2">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.feishu.useCardHint' })}
|
||||
</p>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.feishu.title' })}
|
||||
</label>
|
||||
<Input
|
||||
value={config.title || ''}
|
||||
onChange={(e) => onUpdate({ title: e.target.value })}
|
||||
placeholder="CCW Notification"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== DingTalk Config Form ==========
|
||||
|
||||
function DingTalkConfigForm({
|
||||
config,
|
||||
onUpdate,
|
||||
}: {
|
||||
config: DingTalkConfig;
|
||||
onUpdate: (updates: Partial<DingTalkConfig>) => 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.dingtalk.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://oapi.dingtalk.com/robot/send?access_token=..."
|
||||
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.dingtalk.webhookUrlHint' })}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.dingtalk.keywords' })}
|
||||
</label>
|
||||
<Input
|
||||
value={config.keywords?.join(', ') || ''}
|
||||
onChange={(e) => onUpdate({ keywords: e.target.value.split(',').map(k => k.trim()).filter(Boolean) })}
|
||||
placeholder="keyword1, keyword2"
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.dingtalk.keywordsHint' })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== WeCom Config Form ==========
|
||||
|
||||
function WeComConfigForm({
|
||||
config,
|
||||
onUpdate,
|
||||
}: {
|
||||
config: WeComConfig;
|
||||
onUpdate: (updates: Partial<WeComConfig>) => 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.wecom.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://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=..."
|
||||
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.wecom.webhookUrlHint' })}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.wecom.mentionedList' })}
|
||||
</label>
|
||||
<Input
|
||||
value={config.mentionedList?.join(', ') || ''}
|
||||
onChange={(e) => onUpdate({ mentionedList: e.target.value.split(',').map(m => m.trim()).filter(Boolean) })}
|
||||
placeholder="userid1, userid2, @all"
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.wecom.mentionedListHint' })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Email Config Form ==========
|
||||
|
||||
function EmailConfigForm({
|
||||
config,
|
||||
onUpdate,
|
||||
}: {
|
||||
config: EmailConfig;
|
||||
onUpdate: (updates: Partial<EmailConfig>) => void;
|
||||
}) {
|
||||
const { formatMessage } = useIntl();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.email.host' })}
|
||||
</label>
|
||||
<Input
|
||||
value={config.host || ''}
|
||||
onChange={(e) => onUpdate({ host: e.target.value })}
|
||||
placeholder="smtp.gmail.com"
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.email.hostHint' })}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.email.port' })}
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={config.port || 587}
|
||||
onChange={(e) => onUpdate({ port: parseInt(e.target.value, 10) || 587 })}
|
||||
placeholder="587"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="email-secure"
|
||||
checked={config.secure || false}
|
||||
onChange={(e) => onUpdate({ secure: e.target.checked })}
|
||||
className="rounded border-border"
|
||||
/>
|
||||
<label htmlFor="email-secure" className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.email.secure' })}
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.email.username' })}
|
||||
</label>
|
||||
<Input
|
||||
value={config.username || ''}
|
||||
onChange={(e) => onUpdate({ username: e.target.value })}
|
||||
placeholder="your-email@gmail.com"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.email.password' })}
|
||||
</label>
|
||||
<div className="flex gap-2 mt-1">
|
||||
<Input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={config.password || ''}
|
||||
onChange={(e) => onUpdate({ password: e.target.value })}
|
||||
placeholder="********"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="shrink-0"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.email.from' })}
|
||||
</label>
|
||||
<Input
|
||||
value={config.from || ''}
|
||||
onChange={(e) => onUpdate({ from: e.target.value })}
|
||||
placeholder="noreply@example.com"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.email.to' })}
|
||||
</label>
|
||||
<Input
|
||||
value={config.to?.join(', ') || ''}
|
||||
onChange={(e) => onUpdate({ to: e.target.value.split(',').map(t => t.trim()).filter(Boolean) })}
|
||||
placeholder="user1@example.com, user2@example.com"
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.email.toHint' })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PlatformConfigCards;
|
||||
|
||||
@@ -11,15 +11,13 @@ import {
|
||||
RefreshCw,
|
||||
Check,
|
||||
X,
|
||||
Save,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
TestTube,
|
||||
Save,
|
||||
AlertTriangle,
|
||||
Plus,
|
||||
} 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';
|
||||
@@ -30,6 +28,10 @@ import type {
|
||||
DiscordConfig,
|
||||
TelegramConfig,
|
||||
WebhookConfig,
|
||||
FeishuConfig,
|
||||
DingTalkConfig,
|
||||
WeComConfig,
|
||||
EmailConfig,
|
||||
} from '@/types/remote-notification';
|
||||
import { PLATFORM_INFO, EVENT_INFO, getDefaultConfig } from '@/types/remote-notification';
|
||||
import { PlatformConfigCards } from './PlatformConfigCards';
|
||||
@@ -45,6 +47,7 @@ export function RemoteNotificationSection({ className }: RemoteNotificationSecti
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [testing, setTesting] = useState<NotificationPlatform | null>(null);
|
||||
const [expandedPlatform, setExpandedPlatform] = useState<NotificationPlatform | null>(null);
|
||||
const [expandedEvent, setExpandedEvent] = useState<number | null>(null);
|
||||
|
||||
// Load configuration
|
||||
const loadConfig = useCallback(async () => {
|
||||
@@ -97,7 +100,7 @@ export function RemoteNotificationSection({ className }: RemoteNotificationSecti
|
||||
// Test platform
|
||||
const testPlatform = useCallback(async (
|
||||
platform: NotificationPlatform,
|
||||
platformConfig: DiscordConfig | TelegramConfig | WebhookConfig
|
||||
platformConfig: DiscordConfig | TelegramConfig | WebhookConfig | FeishuConfig | DingTalkConfig | WeComConfig | EmailConfig
|
||||
) => {
|
||||
setTesting(platform);
|
||||
try {
|
||||
@@ -136,7 +139,7 @@ export function RemoteNotificationSection({ className }: RemoteNotificationSecti
|
||||
// Update platform config
|
||||
const updatePlatformConfig = (
|
||||
platform: NotificationPlatform,
|
||||
updates: Partial<DiscordConfig | TelegramConfig | WebhookConfig>
|
||||
updates: Partial<DiscordConfig | TelegramConfig | WebhookConfig | FeishuConfig | DingTalkConfig | WeComConfig | EmailConfig>
|
||||
) => {
|
||||
if (!config) return;
|
||||
const newConfig = {
|
||||
@@ -160,6 +163,19 @@ export function RemoteNotificationSection({ className }: RemoteNotificationSecti
|
||||
setConfig({ ...config, events: newEvents });
|
||||
};
|
||||
|
||||
// Toggle platform for event
|
||||
const toggleEventPlatform = (eventIndex: number, platform: NotificationPlatform) => {
|
||||
if (!config) return;
|
||||
const eventConfig = config.events[eventIndex];
|
||||
const platforms = eventConfig.platforms.includes(platform)
|
||||
? eventConfig.platforms.filter((p) => p !== platform)
|
||||
: [...eventConfig.platforms, platform];
|
||||
updateEventConfig(eventIndex, { platforms });
|
||||
};
|
||||
|
||||
// All available platforms
|
||||
const allPlatforms: NotificationPlatform[] = ['discord', 'telegram', 'feishu', 'dingtalk', 'wecom', 'email', 'webhook'];
|
||||
|
||||
// Reset to defaults
|
||||
const resetConfig = async () => {
|
||||
if (!confirm(formatMessage({ id: 'settings.remoteNotifications.resetConfirm' }))) {
|
||||
@@ -266,51 +282,107 @@ export function RemoteNotificationSection({ className }: RemoteNotificationSecti
|
||||
<div className="grid gap-3">
|
||||
{config.events.map((eventConfig, index) => {
|
||||
const info = EVENT_INFO[eventConfig.event];
|
||||
const isExpanded = expandedEvent === index;
|
||||
return (
|
||||
<div
|
||||
key={eventConfig.event}
|
||||
className="flex items-center justify-between p-3 rounded-lg border border-border bg-muted/30"
|
||||
className="rounded-lg border border-border bg-muted/30 overflow-hidden"
|
||||
>
|
||||
<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>
|
||||
{/* Event Header */}
|
||||
<div
|
||||
className="flex items-center justify-between p-3 cursor-pointer hover:bg-muted/50 transition-colors"
|
||||
onClick={() => setExpandedEvent(isExpanded ? null : index)}
|
||||
>
|
||||
<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>
|
||||
<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" />
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Platform badges */}
|
||||
<div className="flex gap-1 flex-wrap max-w-xs">
|
||||
{eventConfig.platforms.slice(0, 3).map((platform) => (
|
||||
<Badge key={platform} variant="secondary" className="text-xs">
|
||||
{PLATFORM_INFO[platform].name}
|
||||
</Badge>
|
||||
))}
|
||||
{eventConfig.platforms.length > 3 && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
+{eventConfig.platforms.length - 3}
|
||||
</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={(e) => {
|
||||
e.stopPropagation();
|
||||
updateEventConfig(index, { enabled: !eventConfig.enabled });
|
||||
}}
|
||||
>
|
||||
{eventConfig.enabled ? (
|
||||
<Check className="w-3.5 h-3.5" />
|
||||
) : (
|
||||
<X className="w-3.5 h-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
{/* Expand icon */}
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="w-4 h-4 text-muted-foreground" />
|
||||
) : (
|
||||
<X className="w-3.5 h-3.5" />
|
||||
<ChevronDown className="w-4 h-4 text-muted-foreground" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded Content - Platform Selection */}
|
||||
{isExpanded && (
|
||||
<div className="border-t border-border p-4 space-y-3 bg-muted/20">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'settings.remoteNotifications.selectPlatforms' })}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{allPlatforms.map((platform) => {
|
||||
const isSelected = eventConfig.platforms.includes(platform);
|
||||
const platformInfo = PLATFORM_INFO[platform];
|
||||
const platformConfig = config.platforms[platform];
|
||||
const isConfigured = platformConfig?.enabled;
|
||||
return (
|
||||
<Button
|
||||
key={platform}
|
||||
variant={isSelected ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
className={cn(
|
||||
'h-8',
|
||||
!isConfigured && !isSelected && 'opacity-50'
|
||||
)}
|
||||
onClick={() => toggleEventPlatform(index, platform)}
|
||||
>
|
||||
{isSelected && <Check className="w-3 h-3 mr-1" />}
|
||||
{platformInfo.name}
|
||||
{!isConfigured && !isSelected && (
|
||||
<Plus className="w-3 h-3 ml-1 opacity-50" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user