From 9d51e386414876b1007e085e5aaff0de4529c30b Mon Sep 17 00:00:00 2001 From: taohe Date: Thu, 11 Jun 2026 14:37:58 +0800 Subject: [PATCH] Keep configured IM channels editable --- .../gateway/routers/channel_connections.py | 8 +- .../tests/test_channel_connections_router.py | 3 + .../channel-runtime-config-dialog.tsx | 8 +- .../channels/workspace-channels-list.tsx | 19 +++-- .../settings/channels-settings-page.tsx | 73 +++++++++++++------ frontend/src/core/i18n/locales/en-US.ts | 3 + frontend/src/core/i18n/locales/types.ts | 3 + frontend/src/core/i18n/locales/zh-CN.ts | 3 + frontend/tests/e2e/channels.spec.ts | 58 ++++++--------- 9 files changed, 110 insertions(+), 68 deletions(-) diff --git a/backend/app/gateway/routers/channel_connections.py b/backend/app/gateway/routers/channel_connections.py index 68ec522b7..c4f45be5e 100644 --- a/backend/app/gateway/routers/channel_connections.py +++ b/backend/app/gateway/routers/channel_connections.py @@ -284,6 +284,12 @@ def _provider_response( connection: dict[str, Any] | None = None, ) -> ChannelProviderResponse: status, unavailable_reason = _provider_status(config, channels_config, provider) + if connection: + connection_status = connection["status"] + elif status["configured"] and unavailable_reason is None: + connection_status = "connected" + else: + connection_status = "not_connected" return ChannelProviderResponse( provider=provider, display_name=meta["display_name"], @@ -292,7 +298,7 @@ def _provider_response( connectable=status["enabled"] and status["configured"] and unavailable_reason is None, unavailable_reason=unavailable_reason, auth_mode=meta["auth_mode"], - connection_status=connection["status"] if connection else "not_connected", + connection_status=connection_status, credential_fields=_credential_fields(provider), ) diff --git a/backend/tests/test_channel_connections_router.py b/backend/tests/test_channel_connections_router.py index 374210477..108af262e 100644 --- a/backend/tests/test_channel_connections_router.py +++ b/backend/tests/test_channel_connections_router.py @@ -123,10 +123,12 @@ def test_get_providers_uses_existing_channels_config(tmp_path): assert by_provider["telegram"]["auth_mode"] == "deep_link" assert by_provider["slack"]["configured"] is True assert by_provider["slack"]["auth_mode"] == "binding_code" + assert by_provider["slack"]["connection_status"] == "connected" assert by_provider["discord"]["configured"] is True assert by_provider["discord"]["auth_mode"] == "binding_code" assert by_provider["feishu"]["configured"] is True assert by_provider["feishu"]["auth_mode"] == "binding_code" + assert by_provider["feishu"]["connection_status"] == "connected" assert by_provider["dingtalk"]["configured"] is True assert by_provider["dingtalk"]["auth_mode"] == "binding_code" assert by_provider["wechat"]["configured"] is True @@ -384,6 +386,7 @@ def test_configure_provider_runtime_credentials_enables_connect_without_file_edi assert configured["provider"] == "slack" assert configured["configured"] is True assert configured["connectable"] is True + assert configured["connection_status"] == "connected" assert app.state.channels_config["slack"] == { "enabled": True, "bot_token": "xoxb-ui", diff --git a/frontend/src/components/workspace/channels/channel-runtime-config-dialog.tsx b/frontend/src/components/workspace/channels/channel-runtime-config-dialog.tsx index 49a166af1..11753f5e7 100644 --- a/frontend/src/components/workspace/channels/channel-runtime-config-dialog.tsx +++ b/frontend/src/components/workspace/channels/channel-runtime-config-dialog.tsx @@ -60,6 +60,8 @@ export function ChannelRuntimeConfigDialog({ return null; } + const isEditing = provider.configured; + const handleSubmit = (event: FormEvent) => { event.preventDefault(); onSubmit(provider, values); @@ -71,7 +73,9 @@ export function ChannelRuntimeConfigDialog({
- {t.channels.setupTitle(provider.display_name)} + {isEditing + ? t.channels.setupEditTitle(provider.display_name) + : t.channels.setupTitle(provider.display_name)} {t.channels.setupDescription} @@ -118,7 +122,7 @@ export function ChannelRuntimeConfigDialog({ {submitting ? ( ) : null} - {t.channels.saveAndConnect} + {isEditing ? t.channels.saveChanges : t.channels.saveAndConnect} diff --git a/frontend/src/components/workspace/channels/workspace-channels-list.tsx b/frontend/src/components/workspace/channels/workspace-channels-list.tsx index 9fcb36067..897cc79a0 100644 --- a/frontend/src/components/workspace/channels/workspace-channels-list.tsx +++ b/frontend/src/components/workspace/channels/workspace-channels-list.tsx @@ -37,6 +37,10 @@ function providerCanConnect(provider: ChannelProvider): boolean { ); } +function providerCanEditRuntimeConfig(provider: ChannelProvider): boolean { + return provider.enabled && (provider.credential_fields?.length ?? 0) > 0; +} + function getProviderUnavailableReason( provider: ChannelProvider, t: ReturnType["t"], @@ -126,6 +130,7 @@ export function WorkspaceChannelsList() { {t.sidebar.channels} {visibleProviders.map((provider) => { + const canEditRuntimeConfig = providerCanEditRuntimeConfig(provider); const isConnected = provider.connection_status === "connected"; const isPending = (connectMutation.isPending && @@ -153,10 +158,13 @@ export function WorkspaceChannelsList() { "h-8 w-24 px-2 text-xs", isConnected && "gap-1", )} - disabled={isConnected || isPending} + disabled={isPending} title={unavailableReason} onClick={() => { - if (providerNeedsRuntimeConfig(provider)) { + if ( + providerNeedsRuntimeConfig(provider) || + canEditRuntimeConfig + ) { setSetupProvider(provider); return; } @@ -193,16 +201,13 @@ export function WorkspaceChannelsList() { } }} onSubmit={(provider, values) => { - const connectWindow = - provider.auth_mode === "deep_link" ? prepareConnectWindow() : null; void configureMutation .mutateAsync({ provider: provider.provider, values }) - .then((configuredProvider) => { + .then(() => { setSetupProvider(null); - startConnect(configuredProvider, connectWindow); + toast.success(t.channels.connected); }) .catch((error) => { - closeConnectWindow(connectWindow); toast.error( error instanceof Error ? error.message : t.channels.unavailable, ); diff --git a/frontend/src/components/workspace/settings/channels-settings-page.tsx b/frontend/src/components/workspace/settings/channels-settings-page.tsx index f180ef050..8934043a7 100644 --- a/frontend/src/components/workspace/settings/channels-settings-page.tsx +++ b/frontend/src/components/workspace/settings/channels-settings-page.tsx @@ -108,6 +108,10 @@ function providerNeedsRuntimeConfig(provider: ChannelProvider): boolean { ); } +function providerCanEditRuntimeConfig(provider: ChannelProvider): boolean { + return provider.enabled && (provider.credential_fields?.length ?? 0) > 0; +} + function ChannelProviderItem({ provider, connection, @@ -120,7 +124,10 @@ function ChannelProviderItem({ const configureMutation = useConfigureChannelProvider(); const disconnectMutation = useDisconnectChannelConnection(); const [setupOpen, setSetupOpen] = useState(false); - const isConnected = connection?.status === "connected"; + const isConnected = + connection?.status === "connected" || + provider.connection_status === "connected"; + const canEditRuntimeConfig = providerCanEditRuntimeConfig(provider); const canConnect = (provider.connectable ?? (provider.enabled && provider.configured)) && !isConnected; @@ -195,21 +202,41 @@ function ChannelProviderItem({ - {isConnected && connection ? ( - + {isConnected ? ( + <> + {canEditRuntimeConfig ? ( + + ) : null} + {connection ? ( + + ) : null} + ) : (