Keep unavailable channel connect buttons clickable

This commit is contained in:
taohe
2026-06-11 11:28:56 +08:00
parent 89da9b70db
commit 0e939bfe23
3 changed files with 91 additions and 18 deletions
@@ -34,10 +34,13 @@ function providerCanConnect(provider: ChannelProvider): boolean {
);
}
function getProviderDisabledReason(
function getProviderUnavailableReason(
provider: ChannelProvider,
t: ReturnType<typeof useI18n>["t"],
): string | undefined {
if (provider.unavailable_reason) {
return provider.unavailable_reason;
}
if (!provider.enabled) {
return t.channels.disabled;
}
@@ -84,6 +87,7 @@ export function WorkspaceChannelsList() {
connectMutation.isPending &&
connectMutation.variables === provider.provider;
const canConnect = providerCanConnect(provider);
const unavailableReason = getProviderUnavailableReason(provider, t);
return (
<SidebarMenuItem key={provider.provider}>
@@ -103,9 +107,14 @@ export function WorkspaceChannelsList() {
"h-8 w-24 px-2 text-xs",
isConnected && "gap-1",
)}
disabled={!canConnect || isPending}
title={getProviderDisabledReason(provider, t)}
disabled={isConnected || isPending}
title={unavailableReason}
onClick={() => {
if (!canConnect) {
toast.error(unavailableReason ?? t.channels.unavailable);
return;
}
const connectWindow =
provider.auth_mode === "deep_link"
? prepareConnectWindow()
@@ -81,10 +81,13 @@ function getStatusLabel(
return t.channels.notConnected;
}
function getProviderDisabledReason(
function getProviderUnavailableReason(
provider: ChannelProvider,
t: ReturnType<typeof useI18n>["t"],
): string | undefined {
if (provider.unavailable_reason) {
return provider.unavailable_reason;
}
if (!provider.enabled) {
return t.channels.disabled;
}
@@ -116,7 +119,7 @@ function ChannelProviderItem({
disconnectMutation.variables === connection?.id;
const connectionLabel = connection ? getConnectionLabel(connection) : null;
const statusLabel = getStatusLabel(provider, connection, t);
const unavailableReason = getProviderDisabledReason(provider, t);
const unavailableReason = getProviderUnavailableReason(provider, t);
return (
<Item variant="outline" className="w-full items-start">
@@ -162,9 +165,14 @@ function ChannelProviderItem({
<Button
type="button"
size="sm"
disabled={!canConnect || isConnecting}
disabled={isConnecting}
title={unavailableReason}
onClick={() => {
if (!canConnect) {
toast.error(unavailableReason ?? t.channels.unavailable);
return;
}
const connectWindow =
provider.auth_mode === "deep_link"
? prepareConnectWindow()
+67 -11
View File
@@ -12,15 +12,19 @@ const channelProviders = [
["wecom", "WeCom", "binding_code"],
] as const;
function mockChannelsAPI(page: Page) {
void page.route("**/api/channels/providers", (route) => {
return route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
enabled: true,
providers: channelProviders.map(
([provider, displayName, authMode]) => ({
type MockChannelProvider = {
provider: string;
display_name: string;
enabled: boolean;
configured: boolean;
connectable: boolean;
auth_mode: string;
connection_status: string;
unavailable_reason?: string | null;
};
function defaultProviders(): MockChannelProvider[] {
return channelProviders.map(([provider, displayName, authMode]) => ({
provider,
display_name: displayName,
enabled: true,
@@ -28,8 +32,21 @@ function mockChannelsAPI(page: Page) {
connectable: true,
auth_mode: authMode,
connection_status: "not_connected",
}),
),
}));
}
function mockChannelsAPI(
page: Page,
providers: MockChannelProvider[] = defaultProviders(),
onSlackConnect?: () => void,
) {
void page.route("**/api/channels/providers", (route) => {
return route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
enabled: true,
providers,
}),
});
});
@@ -43,6 +60,7 @@ function mockChannelsAPI(page: Page) {
});
void page.route("**/api/channels/slack/connect", (route) => {
onSlackConnect?.();
return route.fulfill({
status: 200,
contentType: "application/json",
@@ -102,4 +120,42 @@ test.describe("IM channels", () => {
page.getByText("Send /connect abc123 to the DeerFlow Slack bot."),
).toBeVisible();
});
test("unavailable providers stay clickable and explain what is missing", async ({
page,
}) => {
mockLangGraphAPI(page);
const unavailableReason =
"Enable and configure channels.slack with channels.slack.bot_token and channels.slack.app_token.";
let connectRequests = 0;
mockChannelsAPI(
page,
[
{
provider: "slack",
display_name: "Slack",
enabled: true,
configured: false,
connectable: false,
unavailable_reason: unavailableReason,
auth_mode: "binding_code",
connection_status: "not_connected",
},
],
() => {
connectRequests += 1;
},
);
await page.goto("/workspace/chats/new");
const sidebar = page.locator("[data-sidebar='sidebar']");
const connectButton = sidebar.getByRole("button", { name: "Connect" });
await expect(connectButton).toBeEnabled({ timeout: 15_000 });
await connectButton.click();
await expect(page.getByText(unavailableReason)).toBeVisible();
expect(connectRequests).toBe(0);
});
});