Refactor channel account management: move binding/editing to Channels, align Agents display, and simplify UX (#523)
This commit is contained in:
@@ -47,7 +47,11 @@ interface ChannelConfigModalProps {
|
||||
configuredTypes?: string[];
|
||||
showChannelName?: boolean;
|
||||
allowExistingConfig?: boolean;
|
||||
allowEditAccountId?: boolean;
|
||||
existingAccountIds?: string[];
|
||||
initialConfigValues?: Record<string, string>;
|
||||
agentId?: string;
|
||||
accountId?: string;
|
||||
onClose: () => void;
|
||||
onChannelSaved?: (channelType: ChannelType) => void | Promise<void>;
|
||||
}
|
||||
@@ -62,7 +66,11 @@ export function ChannelConfigModal({
|
||||
configuredTypes = [],
|
||||
showChannelName = true,
|
||||
allowExistingConfig = true,
|
||||
allowEditAccountId = false,
|
||||
existingAccountIds = [],
|
||||
initialConfigValues,
|
||||
agentId,
|
||||
accountId,
|
||||
onClose,
|
||||
onChannelSaved,
|
||||
}: ChannelConfigModalProps) {
|
||||
@@ -71,6 +79,7 @@ export function ChannelConfigModal({
|
||||
const [selectedType, setSelectedType] = useState<ChannelType | null>(initialSelectedType);
|
||||
const [configValues, setConfigValues] = useState<Record<string, string>>({});
|
||||
const [channelName, setChannelName] = useState('');
|
||||
const [accountIdInput, setAccountIdInput] = useState(accountId || '');
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
const [showSecrets, setShowSecrets] = useState<Record<string, boolean>>({});
|
||||
const [qrCode, setQrCode] = useState<string | null>(null);
|
||||
@@ -86,11 +95,18 @@ export function ChannelConfigModal({
|
||||
|
||||
const meta: ChannelMeta | null = selectedType ? CHANNEL_META[selectedType] : null;
|
||||
const shouldUseCredentialValidation = selectedType !== 'feishu';
|
||||
const resolvedAccountId = allowEditAccountId
|
||||
? accountIdInput.trim()
|
||||
: (accountId ?? (agentId ? (agentId === 'main' ? 'default' : agentId) : undefined));
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedType(initialSelectedType);
|
||||
}, [initialSelectedType]);
|
||||
|
||||
useEffect(() => {
|
||||
setAccountIdInput(accountId || '');
|
||||
}, [accountId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedType) {
|
||||
setConfigValues({});
|
||||
@@ -112,13 +128,21 @@ export function ChannelConfigModal({
|
||||
return;
|
||||
}
|
||||
|
||||
if (initialConfigValues) {
|
||||
setConfigValues(initialConfigValues);
|
||||
setIsExistingConfig(Object.keys(initialConfigValues).length > 0);
|
||||
setLoadingConfig(false);
|
||||
setChannelName(showChannelName ? CHANNEL_NAMES[selectedType] : '');
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
setLoadingConfig(true);
|
||||
setChannelName(showChannelName ? CHANNEL_NAMES[selectedType] : '');
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const accountParam = agentId ? `?accountId=${encodeURIComponent(agentId === 'main' ? 'default' : agentId)}` : '';
|
||||
const accountParam = resolvedAccountId ? `?accountId=${encodeURIComponent(resolvedAccountId)}` : '';
|
||||
const result = await hostApiFetch<{ success: boolean; values?: Record<string, string> }>(
|
||||
`/api/channels/config/${encodeURIComponent(selectedType)}${accountParam}`
|
||||
);
|
||||
@@ -144,7 +168,7 @@ export function ChannelConfigModal({
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [agentId, allowExistingConfig, configuredTypes, selectedType, showChannelName]);
|
||||
}, [allowExistingConfig, configuredTypes, initialConfigValues, resolvedAccountId, selectedType, showChannelName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedType && !loadingConfig && showChannelName && firstInputRef.current) {
|
||||
@@ -187,13 +211,18 @@ export function ChannelConfigModal({
|
||||
try {
|
||||
const saveResult = await hostApiFetch<{ success?: boolean; error?: string }>('/api/channels/config', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ channelType: 'whatsapp', config: { enabled: true } }),
|
||||
body: JSON.stringify({ channelType: 'whatsapp', config: { enabled: true }, accountId: resolvedAccountId }),
|
||||
});
|
||||
if (!saveResult?.success) {
|
||||
throw new Error(saveResult?.error || 'Failed to save WhatsApp config');
|
||||
}
|
||||
|
||||
await finishSave('whatsapp');
|
||||
try {
|
||||
await finishSave('whatsapp');
|
||||
} catch (postSaveError) {
|
||||
toast.warning(t('toast.savedButRefreshFailed'));
|
||||
console.warn('Channel saved but post-save refresh failed:', postSaveError);
|
||||
}
|
||||
// Gateway restart is already triggered by scheduleGatewayChannelRestart
|
||||
// in the POST /api/channels/config route handler (debounced). Calling
|
||||
// restart() here directly races with that debounced restart and the
|
||||
@@ -222,7 +251,7 @@ export function ChannelConfigModal({
|
||||
removeErrorListener();
|
||||
hostApiFetch('/api/channels/whatsapp/cancel', { method: 'POST' }).catch(() => { });
|
||||
};
|
||||
}, [selectedType, finishSave, onClose, t]);
|
||||
}, [finishSave, onClose, resolvedAccountId, selectedType, t]);
|
||||
|
||||
const handleValidate = async () => {
|
||||
if (!selectedType || !shouldUseCredentialValidation) return;
|
||||
@@ -273,10 +302,25 @@ export function ChannelConfigModal({
|
||||
setValidationResult(null);
|
||||
|
||||
try {
|
||||
if (allowEditAccountId) {
|
||||
const nextAccountId = accountIdInput.trim();
|
||||
if (!nextAccountId) {
|
||||
toast.error(t('account.invalidId'));
|
||||
setConnecting(false);
|
||||
return;
|
||||
}
|
||||
const duplicateExists = existingAccountIds.some((id) => id === nextAccountId && id !== (accountId || '').trim());
|
||||
if (duplicateExists) {
|
||||
toast.error(t('account.accountIdExists', { accountId: nextAccountId }));
|
||||
setConnecting(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (meta.connectionType === 'qr') {
|
||||
await hostApiFetch('/api/channels/whatsapp/start', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ accountId: 'default' }),
|
||||
body: JSON.stringify({ accountId: resolvedAccountId || 'default' }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -319,7 +363,6 @@ export function ChannelConfigModal({
|
||||
}
|
||||
|
||||
const config: Record<string, unknown> = { ...configValues };
|
||||
const resolvedAccountId = agentId ? (agentId === 'main' ? 'default' : agentId) : undefined;
|
||||
const saveResult = await hostApiFetch<{
|
||||
success?: boolean;
|
||||
error?: string;
|
||||
@@ -335,7 +378,12 @@ export function ChannelConfigModal({
|
||||
toast.warning(saveResult.warning);
|
||||
}
|
||||
|
||||
await finishSave(selectedType);
|
||||
try {
|
||||
await finishSave(selectedType);
|
||||
} catch (postSaveError) {
|
||||
toast.warning(t('toast.savedButRefreshFailed'));
|
||||
console.warn('Channel saved but post-save refresh failed:', postSaveError);
|
||||
}
|
||||
|
||||
toast.success(t('toast.channelSaved', { name: meta.name }));
|
||||
toast.success(t('toast.channelConnecting', { name: meta.name }));
|
||||
@@ -534,6 +582,20 @@ export function ChannelConfigModal({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{allowEditAccountId && (
|
||||
<div className="space-y-2.5">
|
||||
<Label htmlFor="account-id" className={labelClasses}>{t('account.customIdLabel')}</Label>
|
||||
<Input
|
||||
id="account-id"
|
||||
value={accountIdInput}
|
||||
onChange={(event) => setAccountIdInput(event.target.value)}
|
||||
placeholder={t('account.customIdPlaceholder')}
|
||||
className={inputClasses}
|
||||
/>
|
||||
<p className="text-[12px] text-muted-foreground">{t('account.customIdHint')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
{meta?.configFields.map((field) => (
|
||||
<ConfigField
|
||||
@@ -623,7 +685,7 @@ export function ChannelConfigModal({
|
||||
onClick={() => {
|
||||
void handleConnect();
|
||||
}}
|
||||
disabled={connecting || !isFormValid()}
|
||||
disabled={connecting || !isFormValid() || (allowEditAccountId && !accountIdInput.trim())}
|
||||
className={primaryButtonClasses}
|
||||
>
|
||||
{connecting ? (
|
||||
|
||||
Reference in New Issue
Block a user