@nextclaw/ui 0.5.34 → 0.5.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/dist/assets/ChannelsList-BTQcN7OQ.js +1 -0
- package/dist/assets/{ChatPage-BSD-DBqC.js → ChatPage-B5OG3EW3.js} +2 -2
- package/dist/assets/CronConfig-MXdvM9gu.js +1 -0
- package/dist/assets/{DocBrowser-BCJjJTr9.js → DocBrowser-CJDon901.js} +1 -1
- package/dist/assets/MarketplacePage-BwaTwPfP.js +1 -0
- package/dist/assets/{ModelConfig-iD7V4upL.js → ModelConfig-qJyJ1XS-.js} +1 -1
- package/dist/assets/{ProvidersList-moYj5oBN.js → ProvidersList-DinfLIyS.js} +1 -1
- package/dist/assets/{RuntimeConfig-CvWztear.js → RuntimeConfig-DoKy3o8n.js} +1 -1
- package/dist/assets/{SecretsConfig-Ds5xip01.js → SecretsConfig-S_jppujG.js} +1 -1
- package/dist/assets/SessionsConfig-C5VnCiw_.js +2 -0
- package/dist/assets/{card-CB1zsVbS.js → card-Bsb-eVmY.js} +1 -1
- package/dist/assets/index-BzQBLXUW.js +2 -0
- package/dist/assets/index-DcyOd66N.css +1 -0
- package/dist/assets/{label-d0bFiiuu.js → label-CXP5KktX.js} +1 -1
- package/dist/assets/logos-DqE_6ErA.js +1 -0
- package/dist/assets/{page-layout-Cf2nfjrs.js → page-layout-DMWzimj9.js} +1 -1
- package/dist/assets/{switch-CfELV89t.js → switch-oEZ0AFmj.js} +1 -1
- package/dist/assets/{tabs-custom-CJdhCvOt.js → tabs-custom-CQP93tp3.js} +1 -1
- package/dist/assets/{useConfig-DnBXFnGL.js → useConfig-C0nxJgik.js} +1 -1
- package/dist/assets/useConfirmDialog-d6TTs8io.js +5 -0
- package/dist/assets/{vendor-CmqkRoMs.js → vendor-DN_iJQc4.js} +75 -85
- package/dist/index.html +3 -3
- package/package.json +1 -1
- package/src/api/types.ts +5 -0
- package/src/components/common/LogoBadge.tsx +2 -2
- package/src/components/config/ChannelForm.tsx +185 -185
- package/src/components/config/ChannelsList.tsx +131 -94
- package/src/lib/channel-tutorials.ts +8 -0
- package/src/lib/i18n.ts +6 -0
- package/dist/assets/ChannelsList-DvhJoVmJ.js +0 -1
- package/dist/assets/CronConfig-JLIc66HM.js +0 -1
- package/dist/assets/MarketplacePage-ftvI9vFu.js +0 -1
- package/dist/assets/SessionsConfig-CCbracj_.js +0 -2
- package/dist/assets/dialog-BgbSAXFu.js +0 -5
- package/dist/assets/index-B8Wh_FvS.css +0 -1
- package/dist/assets/index-O1Kus7pd.js +0 -2
- package/dist/assets/logos-gjlYO0d_.js +0 -1
- package/dist/assets/useConfirmDialog-c4_c0EGk.js +0 -1
package/dist/index.html
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
8
8
|
<title>NextClaw - 系统配置</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-
|
|
11
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-BzQBLXUW.js"></script>
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-DN_iJQc4.js">
|
|
11
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DcyOd66N.css">
|
|
12
12
|
</head>
|
|
13
13
|
|
|
14
14
|
<body>
|
package/package.json
CHANGED
package/src/api/types.ts
CHANGED
|
@@ -14,12 +14,12 @@ export function LogoBadge({ name, src, className, imgClassName, fallback }: Logo
|
|
|
14
14
|
const showImage = Boolean(src) && !failed;
|
|
15
15
|
|
|
16
16
|
return (
|
|
17
|
-
<div className={cn('flex items-center justify-center', className)}>
|
|
17
|
+
<div className={cn('inline-flex shrink-0 items-center justify-center overflow-hidden', className)}>
|
|
18
18
|
{showImage ? (
|
|
19
19
|
<img
|
|
20
20
|
src={src as string}
|
|
21
21
|
alt={`${name} logo`}
|
|
22
|
-
className={cn('h-6 w-6 object-contain', imgClassName)}
|
|
22
|
+
className={cn('block h-6 w-6 object-contain', imgClassName)}
|
|
23
23
|
onError={() => setFailed(true)}
|
|
24
24
|
draggable={false}
|
|
25
25
|
/>
|
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
|
-
import { useConfig, useConfigSchema, useUpdateChannel, useExecuteConfigAction } from '@/hooks/useConfig';
|
|
3
|
-
import { useUiStore } from '@/stores/ui.store';
|
|
4
|
-
import {
|
|
5
|
-
Dialog,
|
|
6
|
-
DialogContent,
|
|
7
|
-
DialogHeader,
|
|
8
|
-
DialogTitle,
|
|
9
|
-
DialogDescription,
|
|
10
|
-
DialogFooter,
|
|
11
|
-
} from '@/components/ui/dialog';
|
|
2
|
+
import { useConfig, useConfigMeta, useConfigSchema, useUpdateChannel, useExecuteConfigAction } from '@/hooks/useConfig';
|
|
12
3
|
import { Button } from '@/components/ui/button';
|
|
13
4
|
import { Input } from '@/components/ui/input';
|
|
14
5
|
import { Label } from '@/components/ui/label';
|
|
15
6
|
import { Switch } from '@/components/ui/switch';
|
|
16
7
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
17
8
|
import { TagInput } from '@/components/common/TagInput';
|
|
9
|
+
import { StatusDot } from '@/components/ui/status-dot';
|
|
10
|
+
import { LogoBadge } from '@/components/common/LogoBadge';
|
|
18
11
|
import { t } from '@/lib/i18n';
|
|
19
12
|
import { hintForPath } from '@/lib/config-hints';
|
|
13
|
+
import { cn } from '@/lib/utils';
|
|
20
14
|
import { toast } from 'sonner';
|
|
21
|
-
import {
|
|
15
|
+
import { Settings, ToggleLeft, Hash, Mail, Globe, KeyRound, BookOpen } from 'lucide-react';
|
|
22
16
|
import type { ConfigActionManifest } from '@/api/types';
|
|
17
|
+
import { resolveChannelTutorialUrl } from '@/lib/channel-tutorials';
|
|
18
|
+
import { getChannelLogo } from '@/lib/logos';
|
|
23
19
|
|
|
24
20
|
type ChannelFieldType = 'boolean' | 'text' | 'email' | 'password' | 'number' | 'tags' | 'select' | 'json';
|
|
25
21
|
type ChannelOption = { value: string; label: string };
|
|
26
22
|
type ChannelField = { name: string; type: ChannelFieldType; label: string; options?: ChannelOption[] };
|
|
27
23
|
|
|
24
|
+
type ChannelFormProps = {
|
|
25
|
+
channelName?: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
28
|
const DM_POLICY_OPTIONS: ChannelOption[] = [
|
|
29
29
|
{ value: 'pairing', label: 'pairing' },
|
|
30
30
|
{ value: 'allowlist', label: 'allowlist' },
|
|
@@ -45,7 +45,6 @@ const STREAMING_MODE_OPTIONS: ChannelOption[] = [
|
|
|
45
45
|
{ value: 'progress', label: 'progress' }
|
|
46
46
|
];
|
|
47
47
|
|
|
48
|
-
// Field icon mapping
|
|
49
48
|
const getFieldIcon = (fieldName: string) => {
|
|
50
49
|
if (fieldName.includes('token') || fieldName.includes('secret') || fieldName.includes('password')) {
|
|
51
50
|
return <KeyRound className="h-3.5 w-3.5 text-gray-500" />;
|
|
@@ -163,20 +162,6 @@ function buildChannelFields(): Record<string, ChannelField[]> {
|
|
|
163
162
|
};
|
|
164
163
|
}
|
|
165
164
|
|
|
166
|
-
const channelIcons: Record<string, typeof MessageCircle> = {
|
|
167
|
-
telegram: MessageCircle,
|
|
168
|
-
slack: MessageCircle,
|
|
169
|
-
email: Mail,
|
|
170
|
-
default: MessageCircle
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
const channelColors: Record<string, string> = {
|
|
174
|
-
telegram: 'from-primary-300 to-primary-600',
|
|
175
|
-
slack: 'from-primary-200 to-primary-500',
|
|
176
|
-
email: 'from-primary-100 to-primary-400',
|
|
177
|
-
default: 'from-gray-300 to-gray-500'
|
|
178
|
-
};
|
|
179
|
-
|
|
180
165
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
181
166
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
182
167
|
}
|
|
@@ -207,9 +192,9 @@ function buildScopeDraft(scope: string, value: Record<string, unknown>): Record<
|
|
|
207
192
|
return output;
|
|
208
193
|
}
|
|
209
194
|
|
|
210
|
-
export function ChannelForm() {
|
|
211
|
-
const { channelModal, closeChannelModal } = useUiStore();
|
|
195
|
+
export function ChannelForm({ channelName }: ChannelFormProps) {
|
|
212
196
|
const { data: config } = useConfig();
|
|
197
|
+
const { data: meta } = useConfigMeta();
|
|
213
198
|
const { data: schema } = useConfigSchema();
|
|
214
199
|
const updateChannel = useUpdateChannel();
|
|
215
200
|
const executeAction = useExecuteConfigAction();
|
|
@@ -218,7 +203,6 @@ export function ChannelForm() {
|
|
|
218
203
|
const [jsonDrafts, setJsonDrafts] = useState<Record<string, string>>({});
|
|
219
204
|
const [runningActionId, setRunningActionId] = useState<string | null>(null);
|
|
220
205
|
|
|
221
|
-
const channelName = channelModal.channel;
|
|
222
206
|
const channelConfig = channelName ? config?.channels[channelName] : null;
|
|
223
207
|
const fields = channelName ? buildChannelFields()[channelName] ?? [] : [];
|
|
224
208
|
const uiHints = schema?.uiHints;
|
|
@@ -227,6 +211,8 @@ export function ChannelForm() {
|
|
|
227
211
|
const channelLabel = channelName
|
|
228
212
|
? hintForPath(`channels.${channelName}`, uiHints)?.label ?? channelName
|
|
229
213
|
: channelName;
|
|
214
|
+
const channelMeta = meta?.channels.find((item) => item.name === channelName);
|
|
215
|
+
const tutorialUrl = channelMeta ? resolveChannelTutorialUrl(channelMeta) : undefined;
|
|
230
216
|
|
|
231
217
|
useEffect(() => {
|
|
232
218
|
if (channelConfig) {
|
|
@@ -278,10 +264,7 @@ export function ChannelForm() {
|
|
|
278
264
|
}
|
|
279
265
|
}
|
|
280
266
|
|
|
281
|
-
updateChannel.mutate(
|
|
282
|
-
{ channel: channelName, data: payload },
|
|
283
|
-
{ onSuccess: () => closeChannelModal() }
|
|
284
|
-
);
|
|
267
|
+
updateChannel.mutate({ channel: channelName, data: payload });
|
|
285
268
|
};
|
|
286
269
|
|
|
287
270
|
const applyActionPatchToForm = (patch?: Record<string, unknown>) => {
|
|
@@ -340,163 +323,180 @@ export function ChannelForm() {
|
|
|
340
323
|
}
|
|
341
324
|
};
|
|
342
325
|
|
|
343
|
-
|
|
344
|
-
|
|
326
|
+
if (!channelName || !channelMeta || !channelConfig) {
|
|
327
|
+
return (
|
|
328
|
+
<div className="flex min-h-[520px] items-center justify-center rounded-2xl border border-gray-200/70 bg-white px-6 text-center xl:h-[calc(100vh-180px)] xl:min-h-[600px] xl:max-h-[860px]">
|
|
329
|
+
<div>
|
|
330
|
+
<h3 className="text-base font-semibold text-gray-900">{t('channelsSelectTitle')}</h3>
|
|
331
|
+
<p className="mt-2 text-sm text-gray-500">{t('channelsSelectDescription')}</p>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const enabled = Boolean(channelConfig.enabled);
|
|
345
338
|
|
|
346
339
|
return (
|
|
347
|
-
<
|
|
348
|
-
<
|
|
349
|
-
<
|
|
350
|
-
<div className="
|
|
351
|
-
<div className=
|
|
352
|
-
<
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
340
|
+
<div className="flex min-h-[520px] flex-col rounded-2xl border border-gray-200/70 bg-white shadow-card xl:h-[calc(100vh-180px)] xl:min-h-[600px] xl:max-h-[860px]">
|
|
341
|
+
<div className="border-b border-gray-100 px-6 py-5">
|
|
342
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
343
|
+
<div className="min-w-0">
|
|
344
|
+
<div className="flex items-center gap-3">
|
|
345
|
+
<LogoBadge
|
|
346
|
+
name={channelName}
|
|
347
|
+
src={getChannelLogo(channelName)}
|
|
348
|
+
className={cn(
|
|
349
|
+
'h-9 w-9 rounded-lg border',
|
|
350
|
+
enabled ? 'border-primary/30 bg-white' : 'border-gray-200/70 bg-white'
|
|
351
|
+
)}
|
|
352
|
+
imgClassName="h-5 w-5 object-contain"
|
|
353
|
+
fallback={<span className="text-sm font-semibold uppercase text-gray-500">{channelName[0]}</span>}
|
|
354
|
+
/>
|
|
355
|
+
<h3 className="truncate text-lg font-semibold text-gray-900 capitalize">{channelLabel}</h3>
|
|
357
356
|
</div>
|
|
357
|
+
<p className="mt-2 text-sm text-gray-500">{t('channelsFormDescription')}</p>
|
|
358
|
+
{tutorialUrl && (
|
|
359
|
+
<a
|
|
360
|
+
href={tutorialUrl}
|
|
361
|
+
className="mt-2 inline-flex items-center gap-1.5 text-xs text-primary transition-colors hover:text-primary-hover"
|
|
362
|
+
>
|
|
363
|
+
<BookOpen className="h-3.5 w-3.5" />
|
|
364
|
+
{t('channelsGuideTitle')}
|
|
365
|
+
</a>
|
|
366
|
+
)}
|
|
358
367
|
</div>
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
onCheckedChange={(checked) => updateField(field.name, checked)}
|
|
389
|
-
className="data-[state=checked]:bg-emerald-500"
|
|
390
|
-
/>
|
|
391
|
-
</div>
|
|
392
|
-
)}
|
|
393
|
-
|
|
394
|
-
{(field.type === 'text' || field.type === 'email') && (
|
|
395
|
-
<Input
|
|
396
|
-
id={field.name}
|
|
397
|
-
type={field.type}
|
|
398
|
-
value={(formData[field.name] as string) || ''}
|
|
399
|
-
onChange={(e) => updateField(field.name, e.target.value)}
|
|
400
|
-
placeholder={placeholder}
|
|
401
|
-
className="rounded-xl"
|
|
402
|
-
/>
|
|
403
|
-
)}
|
|
404
|
-
|
|
405
|
-
{field.type === 'password' && (
|
|
406
|
-
<Input
|
|
407
|
-
id={field.name}
|
|
408
|
-
type="password"
|
|
409
|
-
value={(formData[field.name] as string) || ''}
|
|
410
|
-
onChange={(e) => updateField(field.name, e.target.value)}
|
|
411
|
-
placeholder={placeholder ?? t('leaveBlankToKeepUnchanged')}
|
|
412
|
-
className="rounded-xl"
|
|
413
|
-
/>
|
|
414
|
-
)}
|
|
415
|
-
|
|
416
|
-
{field.type === 'number' && (
|
|
417
|
-
<Input
|
|
418
|
-
id={field.name}
|
|
419
|
-
type="number"
|
|
420
|
-
value={(formData[field.name] as number) || 0}
|
|
421
|
-
onChange={(e) => updateField(field.name, parseInt(e.target.value) || 0)}
|
|
422
|
-
placeholder={placeholder}
|
|
423
|
-
className="rounded-xl"
|
|
424
|
-
/>
|
|
425
|
-
)}
|
|
426
|
-
|
|
427
|
-
{field.type === 'tags' && (
|
|
428
|
-
<TagInput
|
|
429
|
-
value={(formData[field.name] as string[]) || []}
|
|
430
|
-
onChange={(tags) => updateField(field.name, tags)}
|
|
431
|
-
/>
|
|
432
|
-
)}
|
|
433
|
-
|
|
434
|
-
{field.type === 'select' && (
|
|
435
|
-
<Select
|
|
436
|
-
value={(formData[field.name] as string) || ''}
|
|
437
|
-
onValueChange={(v) => updateField(field.name, v)}
|
|
438
|
-
>
|
|
439
|
-
<SelectTrigger className="rounded-xl">
|
|
440
|
-
<SelectValue />
|
|
441
|
-
</SelectTrigger>
|
|
442
|
-
<SelectContent>
|
|
443
|
-
{(field.options ?? []).map((option) => (
|
|
444
|
-
<SelectItem key={option.value} value={option.value}>
|
|
445
|
-
{option.label}
|
|
446
|
-
</SelectItem>
|
|
447
|
-
))}
|
|
448
|
-
</SelectContent>
|
|
449
|
-
</Select>
|
|
450
|
-
)}
|
|
451
|
-
|
|
452
|
-
{field.type === 'json' && (
|
|
453
|
-
<textarea
|
|
368
|
+
<StatusDot status={enabled ? 'active' : 'inactive'} label={enabled ? t('statusActive') : t('statusInactive')} />
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
371
|
+
|
|
372
|
+
<form onSubmit={handleSubmit} className="flex min-h-0 flex-1 flex-col">
|
|
373
|
+
<div className="min-h-0 flex-1 space-y-5 overflow-y-auto px-6 py-5">
|
|
374
|
+
{fields.map((field) => {
|
|
375
|
+
const hint = channelName
|
|
376
|
+
? hintForPath(`channels.${channelName}.${field.name}`, uiHints)
|
|
377
|
+
: undefined;
|
|
378
|
+
const label = hint?.label ?? field.label;
|
|
379
|
+
const placeholder = hint?.placeholder;
|
|
380
|
+
|
|
381
|
+
return (
|
|
382
|
+
<div key={field.name} className="space-y-2.5">
|
|
383
|
+
<Label
|
|
384
|
+
htmlFor={field.name}
|
|
385
|
+
className="flex items-center gap-2 text-sm font-medium text-gray-900"
|
|
386
|
+
>
|
|
387
|
+
{getFieldIcon(field.name)}
|
|
388
|
+
{label}
|
|
389
|
+
</Label>
|
|
390
|
+
|
|
391
|
+
{field.type === 'boolean' && (
|
|
392
|
+
<div className="flex items-center justify-between rounded-xl bg-gray-50 p-3">
|
|
393
|
+
<span className="text-sm text-gray-500">
|
|
394
|
+
{(formData[field.name] as boolean) ? t('enabled') : t('disabled')}
|
|
395
|
+
</span>
|
|
396
|
+
<Switch
|
|
454
397
|
id={field.name}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
...prev,
|
|
459
|
-
[field.name]: event.target.value
|
|
460
|
-
}))
|
|
461
|
-
}
|
|
462
|
-
className="min-h-[120px] w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-mono"
|
|
398
|
+
checked={(formData[field.name] as boolean) || false}
|
|
399
|
+
onCheckedChange={(checked) => updateField(field.name, checked)}
|
|
400
|
+
className="data-[state=checked]:bg-emerald-500"
|
|
463
401
|
/>
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
402
|
+
</div>
|
|
403
|
+
)}
|
|
404
|
+
|
|
405
|
+
{(field.type === 'text' || field.type === 'email') && (
|
|
406
|
+
<Input
|
|
407
|
+
id={field.name}
|
|
408
|
+
type={field.type}
|
|
409
|
+
value={(formData[field.name] as string) || ''}
|
|
410
|
+
onChange={(e) => updateField(field.name, e.target.value)}
|
|
411
|
+
placeholder={placeholder}
|
|
412
|
+
className="rounded-xl"
|
|
413
|
+
/>
|
|
414
|
+
)}
|
|
415
|
+
|
|
416
|
+
{field.type === 'password' && (
|
|
417
|
+
<Input
|
|
418
|
+
id={field.name}
|
|
419
|
+
type="password"
|
|
420
|
+
value={(formData[field.name] as string) || ''}
|
|
421
|
+
onChange={(e) => updateField(field.name, e.target.value)}
|
|
422
|
+
placeholder={placeholder ?? t('leaveBlankToKeepUnchanged')}
|
|
423
|
+
className="rounded-xl"
|
|
424
|
+
/>
|
|
425
|
+
)}
|
|
426
|
+
|
|
427
|
+
{field.type === 'number' && (
|
|
428
|
+
<Input
|
|
429
|
+
id={field.name}
|
|
430
|
+
type="number"
|
|
431
|
+
value={(formData[field.name] as number) || 0}
|
|
432
|
+
onChange={(e) => updateField(field.name, parseInt(e.target.value, 10) || 0)}
|
|
433
|
+
placeholder={placeholder}
|
|
434
|
+
className="rounded-xl"
|
|
435
|
+
/>
|
|
436
|
+
)}
|
|
437
|
+
|
|
438
|
+
{field.type === 'tags' && (
|
|
439
|
+
<TagInput
|
|
440
|
+
value={(formData[field.name] as string[]) || []}
|
|
441
|
+
onChange={(tags) => updateField(field.name, tags)}
|
|
442
|
+
/>
|
|
443
|
+
)}
|
|
444
|
+
|
|
445
|
+
{field.type === 'select' && (
|
|
446
|
+
<Select
|
|
447
|
+
value={(formData[field.name] as string) || ''}
|
|
448
|
+
onValueChange={(v) => updateField(field.name, v)}
|
|
449
|
+
>
|
|
450
|
+
<SelectTrigger className="rounded-xl">
|
|
451
|
+
<SelectValue />
|
|
452
|
+
</SelectTrigger>
|
|
453
|
+
<SelectContent>
|
|
454
|
+
{(field.options ?? []).map((option) => (
|
|
455
|
+
<SelectItem key={option.value} value={option.value}>
|
|
456
|
+
{option.label}
|
|
457
|
+
</SelectItem>
|
|
458
|
+
))}
|
|
459
|
+
</SelectContent>
|
|
460
|
+
</Select>
|
|
461
|
+
)}
|
|
462
|
+
|
|
463
|
+
{field.type === 'json' && (
|
|
464
|
+
<textarea
|
|
465
|
+
id={field.name}
|
|
466
|
+
value={jsonDrafts[field.name] ?? '{}'}
|
|
467
|
+
onChange={(event) =>
|
|
468
|
+
setJsonDrafts((prev) => ({
|
|
469
|
+
...prev,
|
|
470
|
+
[field.name]: event.target.value
|
|
471
|
+
}))
|
|
472
|
+
}
|
|
473
|
+
className="min-h-[120px] w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-mono"
|
|
474
|
+
/>
|
|
475
|
+
)}
|
|
476
|
+
</div>
|
|
477
|
+
);
|
|
478
|
+
})}
|
|
479
|
+
</div>
|
|
480
|
+
|
|
481
|
+
<div className="flex flex-wrap items-center justify-end gap-2 border-t border-gray-100 px-6 py-4">
|
|
482
|
+
{actions
|
|
483
|
+
.filter((action) => action.trigger === 'manual')
|
|
484
|
+
.map((action) => (
|
|
485
|
+
<Button
|
|
486
|
+
key={action.id}
|
|
487
|
+
type="button"
|
|
488
|
+
onClick={() => handleManualAction(action)}
|
|
489
|
+
disabled={updateChannel.isPending || Boolean(runningActionId)}
|
|
490
|
+
variant="secondary"
|
|
491
|
+
>
|
|
492
|
+
{runningActionId === action.id ? t('connecting') : action.title}
|
|
493
|
+
</Button>
|
|
494
|
+
))}
|
|
495
|
+
<Button type="submit" disabled={updateChannel.isPending || Boolean(runningActionId)}>
|
|
496
|
+
{updateChannel.isPending ? t('saving') : t('save')}
|
|
497
|
+
</Button>
|
|
498
|
+
</div>
|
|
499
|
+
</form>
|
|
500
|
+
</div>
|
|
501
501
|
);
|
|
502
502
|
}
|