From 4f9342711d86e700efd65ffe0cf4e4bfeedf555c Mon Sep 17 00:00:00 2001 From: KonghaYao <3446798488@qq.com> Date: Fri, 11 Sep 2026 10:11:26 +0800 Subject: [PATCH 1/6] =?UTF-8?q?fix:=20=E5=88=A0=E9=99=A4=20Agent=20?= =?UTF-8?q?=E6=97=B6=E5=85=B3=E9=97=AD=E6=AE=8B=E7=95=99=20ACP=20=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: gpt-5.6-sol --- src/__tests__/round43-acp-ws-handler.test.ts | 27 +++++++++++++++++++ src/services/config/agent-config.ts | 12 ++++++--- src/services/environment-core.ts | 2 ++ src/transport/acp-ws-handler.ts | 28 ++++++++++++++++++++ 4 files changed, 65 insertions(+), 4 deletions(-) diff --git a/src/__tests__/round43-acp-ws-handler.test.ts b/src/__tests__/round43-acp-ws-handler.test.ts index 3455365a..9a1d10b2 100644 --- a/src/__tests__/round43-acp-ws-handler.test.ts +++ b/src/__tests__/round43-acp-ws-handler.test.ts @@ -10,6 +10,7 @@ import { stubRegistryHeartbeat, } from "../test-utils/helpers"; import { + closeAcpConnectionsForEnvironments, closeAllAcpConnections, findMachineConnectionByAgentId, findMachineConnectionById, @@ -152,6 +153,32 @@ describe("round43 acp ws handler", () => { expect(listAcpConnections().find((item) => item.wsId === "unknown-43")).toBeUndefined(); }); + // 删除 Environment 必须关闭本地绑定连接,防止客户端继续用失效环境重连/发请求。 + test("关闭已删除 Environment 的 ACP 连接并清理缓存", () => { + const ws = new FakeWs(); + handleAcpWsOpen(ws, "deleted-env-ws", "user-a", "env-deleted", false); + setAgentMachineCache("env-deleted", "machine-deleted"); + + closeAcpConnectionsForEnvironments(["env-deleted"]); + + expect(ws.closed).toEqual([[1000, "environment deleted"]]); + expect(listAcpConnections().find((item) => item.wsId === "deleted-env-ws")).toBeUndefined(); + expect(getAgentMachineCache().has("env-deleted")).toBe(false); + }); + + // 删除一个 Environment 不得影响其他环境或 machine 连接。 + test("关闭 Environment 连接时保留其他连接", () => { + const deletedWs = new FakeWs(); + const retainedWs = new FakeWs(); + handleAcpWsOpen(deletedWs, "deleted-env-ws-2", "user-a", "env-deleted-2", false); + handleAcpWsOpen(retainedWs, "retained-env-ws", "user-a", "env-retained", false); + + closeAcpConnectionsForEnvironments(["env-deleted-2"]); + + expect(deletedWs.closed).toHaveLength(1); + expect(retainedWs.closed).toEqual([]); + expect(listAcpConnections().map((item) => item.wsId)).toEqual(["retained-env-ws"]); + }); // machine 连接只保存调用者身份,不会复用其他连接的用户标识。 test("machine 快照按连接保留各自用户身份", () => { handleAcpWsOpen(new FakeWs(), "machine-user-a", "user-a", null, true); diff --git a/src/services/config/agent-config.ts b/src/services/config/agent-config.ts index fd60c39d..ddcb25b4 100644 --- a/src/services/config/agent-config.ts +++ b/src/services/config/agent-config.ts @@ -250,15 +250,19 @@ export async function deleteAgentConfig(ctx: AuthContext, name: string): Promise .from(environment) .where(and(eq(environment.organizationId, row.organizationId), eq(environment.agentConfigId, row.id))); if (boundEnvs.length > 0) { + const environmentIds = boundEnvs.map((env) => env.id); + // 先关闭本地 ACP 连接,再停止运行实例;否则客户端仍会用已删除环境继续发消息。 + const { closeAcpConnectionsForEnvironments } = await import("../../transport/acp-ws-handler"); + closeAcpConnectionsForEnvironments(environmentIds); + // 动态 import 打破模块循环:orchestration-instance 顶层静态 import ./config 的 // getReadableAgentConfigById(本文件所在 index 的 re-export),此处若静态反向 // import 会形成 agent-config → orchestration-instance → config 的循环依赖 // (同 orchestration-instance 内 reclaimYjsDocs 的惰性导入模式)。 const { stopInstancesForEnvironments } = await import("../orchestration-instance"); - await stopInstancesForEnvironments( - boundEnvs.map((env) => env.id), - { organizationId: row.organizationId }, - ); + await stopInstancesForEnvironments(environmentIds, { + organizationId: row.organizationId, + }); } return db.transaction(async (tx) => { diff --git a/src/services/environment-core.ts b/src/services/environment-core.ts index 0db758f4..df45ca0e 100644 --- a/src/services/environment-core.ts +++ b/src/services/environment-core.ts @@ -114,6 +114,8 @@ export async function getOwnedEnvironment( * 已校验归属)均在此前完成权限校验,环境内实例必然同属该环境。 */ export async function deleteEnvironment(envId: string): Promise { + const { closeAcpConnectionsForEnvironments } = await import("../transport/acp-ws-handler"); + closeAcpConnectionsForEnvironments([envId]); await stopInstancesForEnvironments([envId]); return environmentRepo.delete(envId); } diff --git a/src/transport/acp-ws-handler.ts b/src/transport/acp-ws-handler.ts index 4a1da235..88c5277d 100644 --- a/src/transport/acp-ws-handler.ts +++ b/src/transport/acp-ws-handler.ts @@ -582,6 +582,34 @@ export function triggerMachineCleanupByMachineId(machineId: string, reason: stri }); } +/** + * 关闭指定 Environment 关联的本地 ACP 连接,并清理 agent → machine 缓存。 + * + * 删除 Environment/Agent 后连接端仍可能继续发送消息;只删除 DB 和 runtime instance + * 不会使这类连接失效,导致服务端持续用已删除的 environmentId 处理请求。 + */ +export function closeAcpConnectionsForEnvironments(environmentIds: string[]): void { + if (environmentIds.length === 0) return; + const environmentIdSet = new Set(environmentIds); + + for (const [wsId, entry] of connections) { + if (entry.isMachine || !entry.boundEnvId || !environmentIdSet.has(entry.boundEnvId)) continue; + + if (entry.unsub) entry.unsub(); + if (entry.keepalive) clearInterval(entry.keepalive); + connections.delete(wsId); + try { + entry.ws.close(1000, "environment deleted"); + } catch (error) { + logError(`[ACP-WS-CLOSE] failed to close deleted environment connection: wsId=${wsId}`, error); + } + } + + for (const environmentId of environmentIds) { + agentMachineCache.delete(environmentId); + } +} + /** Called from onClose — marks agent offline and cleans up */ export function handleAcpWsClose(_ws: WsConnection, wsId: string, code?: number, reason?: string): void { const entry = connections.get(wsId); From 0219ede6efcd5abf3e07a6bebc3c0c631b01768f Mon Sep 17 00:00:00 2001 From: KonghaYao <3446798488@qq.com> Date: Fri, 11 Sep 2026 10:38:47 +0800 Subject: [PATCH 2/6] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=81=8A=E5=A4=A9?= =?UTF-8?q?=E5=AE=9A=E4=BD=8D=E4=B8=8E=E5=AE=9E=E4=BE=8B=E9=80=89=E4=B8=AD?= =?UTF-8?q?=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: gpt-5.6-sol --- web/components/chat/SystemMessage.tsx | 36 +++++++++++++++---- web/components/chat/chat-navigation-aids.css | 20 +++++++++++ web/components/chat/chat-navigation-aids.tsx | 6 ++++ .../agent-sidebar-instance-order.test.ts | 6 ++++ .../__tests__/chat-navigation-aids.test.tsx | 2 ++ web/src/__tests__/message.ssr.test.tsx | 9 +++-- web/src/i18n/locales/en/components.json | 2 ++ web/src/i18n/locales/zh/components.json | 2 ++ .../pages/agent-panel/AgentPanelLayout.tsx | 1 + .../chat-design-messages-tools.css | 6 ++++ 10 files changed, 81 insertions(+), 9 deletions(-) diff --git a/web/components/chat/SystemMessage.tsx b/web/components/chat/SystemMessage.tsx index 4c69219b..d28a4604 100644 --- a/web/components/chat/SystemMessage.tsx +++ b/web/components/chat/SystemMessage.tsx @@ -1,24 +1,48 @@ -import { memo } from "react"; +import { memo, useState } from "react"; import { useTranslation } from "react-i18next"; import { NS } from "../../src/i18n"; import { cn } from "../../src/lib/utils"; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "../ui/dialog"; interface SystemMessageProps { - /** 原始 system-reminder 块仅用于判空,内容不在界面暴露。 */ + /** 原始 system-reminder 块仅在用户主动查看详情时展示。 */ rawText: string; className?: string; } /** - * 系统消息仅展示弱化胶囊,不展开内部注入内容。 + * 系统消息默认展示弱化胶囊,双击后可检查完整原始内容。 */ export const SystemMessage = memo(function SystemMessage({ rawText, className }: SystemMessageProps) { const { t } = useTranslation(NS.COMPONENTS); + const [detailsOpen, setDetailsOpen] = useState(false); if (!rawText) return null; return ( -
- {t("messageBubble.systemMessage")} -
+ <> +
+ +
+ + + + {t("messageBubble.systemMessage")} + {t("messageBubble.systemMessageDescription")} + +
+            {rawText}
+          
+
+
+ ); }); diff --git a/web/components/chat/chat-navigation-aids.css b/web/components/chat/chat-navigation-aids.css index 4278b8dd..5a731414 100644 --- a/web/components/chat/chat-navigation-aids.css +++ b/web/components/chat/chat-navigation-aids.css @@ -68,6 +68,21 @@ background: #202936; } +.chat-entry--active-prompt { + border-radius: 12px; + animation: chat-active-prompt-flash 900ms ease-out; +} + +@keyframes chat-active-prompt-flash { + 0%, + 20% { + background-color: rgb(100 116 139 / 14%); + } + 100% { + background-color: transparent; + } +} + .chat-prompt-jump-index__preview { position: fixed; z-index: 30; @@ -109,6 +124,11 @@ } @media (prefers-reduced-motion: reduce) { + .chat-entry--active-prompt { + animation: none; + background-color: rgb(100 116 139 / 10%); + } + .chat-prompt-jump-index__tick, .chat-prompt-jump-index__preview { transition: none; diff --git a/web/components/chat/chat-navigation-aids.tsx b/web/components/chat/chat-navigation-aids.tsx index c0f73de8..210e6ddf 100644 --- a/web/components/chat/chat-navigation-aids.tsx +++ b/web/components/chat/chat-navigation-aids.tsx @@ -57,6 +57,12 @@ export function PromptJumpRail({ entries }: PromptJumpRailProps) { } }, [activeId, visiblePrompts]); + useEffect(() => { + const activePrompt = activeId ? document.getElementById(`chat-entry-${activeId}`) : null; + activePrompt?.classList.add("chat-entry--active-prompt"); + return () => activePrompt?.classList.remove("chat-entry--active-prompt"); + }, [activeId]); + useEffect(() => { const rail = railRef.current; const conversation = rail?.parentElement; diff --git a/web/src/__tests__/agent-sidebar-instance-order.test.ts b/web/src/__tests__/agent-sidebar-instance-order.test.ts index 14548f0b..0442ec8e 100644 --- a/web/src/__tests__/agent-sidebar-instance-order.test.ts +++ b/web/src/__tests__/agent-sidebar-instance-order.test.ts @@ -38,6 +38,12 @@ describe("Agent sidebar Instance 排序", () => { }); }); +test("聊天路由向侧边栏传递当前 Instance", () => { + const source = readFileSync(resolve(import.meta.dir, "../pages/agent-panel/AgentPanelLayout.tsx"), "utf8"); + + expect(source).toContain("selectedInstanceId={isChatRoute ? chatSessionId : lastChatSessionRef.current}"); +}); + // 配置导航分区只允许内容容器滚动,避免与外层包装形成嵌套双滚动条。 test("配置导航分区保持单一滚动容器", () => { const source = readFileSync(resolve(import.meta.dir, "../pages/agent-panel/agent-panel.css"), "utf8"); diff --git a/web/src/__tests__/chat-navigation-aids.test.tsx b/web/src/__tests__/chat-navigation-aids.test.tsx index 0d6c4bfa..4d795e3f 100644 --- a/web/src/__tests__/chat-navigation-aids.test.tsx +++ b/web/src/__tests__/chat-navigation-aids.test.tsx @@ -135,5 +135,7 @@ describe("PromptJumpRail", () => { expect(scrollIntoView).toHaveBeenCalledWith({ behavior: "smooth", block: "center" }); expect(button.getAttribute("aria-current")).toBe("location"); expect(button.getAttribute("aria-controls")).toBe("chat-entry-prompt-1"); + expect(target.classList.contains("chat-entry--active-prompt")).toBe(true); + expect(document.getElementById("chat-entry-prompt-0")?.classList.contains("chat-entry--active-prompt")).toBe(false); }); }); diff --git a/web/src/__tests__/message.ssr.test.tsx b/web/src/__tests__/message.ssr.test.tsx index 36ad8c42..c4f6b229 100644 --- a/web/src/__tests__/message.ssr.test.tsx +++ b/web/src/__tests__/message.ssr.test.tsx @@ -172,20 +172,23 @@ describe("消息组件的服务端渲染", () => { expect(markup).toContain("chat-system-reminder"); }); - // 系统消息只展示标签,并与助手消息正文左边界对齐;原始注入内容不得暴露。 - test("系统消息左对齐且不暴露原始内容", () => { + // 系统消息默认只展示标签并与助手消息正文左边界对齐,原始注入内容仅在主动查看时出现。 + test("系统消息默认隐藏原始内容", () => { const markup = renderToStaticMarkup( createElement(SystemMessage, { rawText: "不可展示" }), ); expect(markup).toContain('class="flex justify-start"'); + expect(markup).toContain("messageBubble.openSystemMessage"); expect(markup).not.toContain("不可展示"); }); - // system-reminder 标签必须提供中英文资源,不能在中文界面继续显示硬编码英文。 + // system-reminder 标签和详情交互必须提供中英文资源,不能在中文界面继续显示硬编码英文。 test("系统消息标签提供中英文翻译", () => { expect(componentsZH.messageBubble.systemMessage).toBe("系统提醒"); + expect(componentsZH.messageBubble.openSystemMessage).toBe("双击查看系统提醒详情"); expect(componentsEN.messageBubble.systemMessage).toBe("SYSTEM REMINDER"); + expect(componentsEN.messageBubble.openSystemMessage).toBe("Double-click to view system reminder details"); }); // 子 Agent 详情默认折叠,只展示执行轨迹摘要,避免占满父工具调用。 diff --git a/web/src/i18n/locales/en/components.json b/web/src/i18n/locales/en/components.json index 0875fa75..63f46c32 100644 --- a/web/src/i18n/locales/en/components.json +++ b/web/src/i18n/locales/en/components.json @@ -284,6 +284,8 @@ "uploadedImage": "Uploaded image", "imagePreview": "Image preview", "systemMessage": "SYSTEM REMINDER", + "openSystemMessage": "Double-click to view system reminder details", + "systemMessageDescription": "This system-injected content is shown only for inspecting the current conversation context.", "turnError": "Execution failed", "actions": "Message actions", "copy": "Copy", diff --git a/web/src/i18n/locales/zh/components.json b/web/src/i18n/locales/zh/components.json index 4ddcd20b..a94532b9 100644 --- a/web/src/i18n/locales/zh/components.json +++ b/web/src/i18n/locales/zh/components.json @@ -284,6 +284,8 @@ "uploadedImage": "已上传图片", "imagePreview": "图片预览", "systemMessage": "系统提醒", + "openSystemMessage": "双击查看系统提醒详情", + "systemMessageDescription": "以下内容由系统注入,仅供检查当前会话上下文。", "turnError": "执行出错", "actions": "消息操作", "copy": "复制", diff --git a/web/src/pages/agent-panel/AgentPanelLayout.tsx b/web/src/pages/agent-panel/AgentPanelLayout.tsx index fe7db06b..86f46d00 100644 --- a/web/src/pages/agent-panel/AgentPanelLayout.tsx +++ b/web/src/pages/agent-panel/AgentPanelLayout.tsx @@ -125,6 +125,7 @@ export function AgentPanelLayout() { setCreateDialogOpen(true)} diff --git a/web/src/pages/agent-panel/chat-design-messages-tools.css b/web/src/pages/agent-panel/chat-design-messages-tools.css index 2334ebdb..f02535b6 100644 --- a/web/src/pages/agent-panel/chat-design-messages-tools.css +++ b/web/src/pages/agent-panel/chat-design-messages-tools.css @@ -230,6 +230,12 @@ 700 10px / 1.4 ui-monospace, monospace; letter-spacing: 0.04em; + cursor: pointer; +} +.chat-system-reminder:hover, +.chat-system-reminder:focus-visible { + border-color: #c5cfdd; + color: #46566d; } .message-content ul { From 1f0cf04408d7423511759cda0ac92055873e494f Mon Sep 17 00:00:00 2001 From: KonghaYao <3446798488@qq.com> Date: Fri, 11 Sep 2026 10:43:41 +0800 Subject: [PATCH 3/6] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=BB=84=E7=BB=87?= =?UTF-8?q?=E9=82=80=E8=AF=B7=E5=80=99=E9=80=89=E7=94=A8=E6=88=B7=E9=80=89?= =?UTF-8?q?=E6=8B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: gpt-5.6-sol --- .../organization-invite-dialog.test.tsx | 90 +++++++++++++++ .../pages/agent-organizations-dialogs.tsx | 105 ++++++++++-------- .../agent-panel/pages/agent-organizations.css | 38 +++++++ 3 files changed, 188 insertions(+), 45 deletions(-) create mode 100644 web/src/__tests__/organization-invite-dialog.test.tsx diff --git a/web/src/__tests__/organization-invite-dialog.test.tsx b/web/src/__tests__/organization-invite-dialog.test.tsx new file mode 100644 index 00000000..0618367f --- /dev/null +++ b/web/src/__tests__/organization-invite-dialog.test.tsx @@ -0,0 +1,90 @@ +import { afterAll, afterEach, describe, expect, test } from "bun:test"; +import { Window } from "happy-dom"; +import { act } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { I18nextProvider } from "react-i18next"; +import type { OrgMemberCandidate } from "../api/organizations"; +import i18n from "../i18n"; +import { initializeHappyDomWindow } from "./happy-dom-window"; + +(globalThis as Record).IS_REACT_ACT_ENVIRONMENT = true; +const win = initializeHappyDomWindow(new Window()); +const globals = globalThis as Record; +const originalGlobals = new Map( + ["window", "document", "navigator", "HTMLElement", "Element", "Node", "CustomEvent"].map((key) => [ + key, + globals[key], + ]), +); +globals.window = win; +globals.document = win.document; +globals.navigator = win.navigator; +globals.HTMLElement = win.HTMLElement; +globals.Element = win.Element; +globals.Node = win.Node; +globals.CustomEvent = win.CustomEvent; + +const candidate: OrgMemberCandidate = { + id: "user-2", + name: "2222", + email: "admin1@test.com", + isMember: false, +}; + +let root: Root | null = null; +afterEach(() => { + act(() => root?.unmount()); + root = null; + win.document.body.replaceChildren(); +}); +afterAll(() => { + for (const [key, value] of originalGlobals) { + if (value === undefined) delete globals[key]; + else globals[key] = value; + } +}); + +describe("添加组织成员弹窗", () => { + // 点击搜索结果必须触发候选人选择,避免添加按钮一直停留在禁用状态。 + test("点击候选用户触发添加回调", async () => { + const { MemberCandidateButton } = await import("../pages/agent-panel/pages/agent-organizations-dialogs"); + const selected: OrgMemberCandidate[] = []; + const container = win.document.createElement("div"); + win.document.body.appendChild(container); + root = createRoot(container as unknown as HTMLElement); + await act(async () => + root?.render( + + selected.push(item)} /> + , + ), + ); + + const button = container.querySelector("button") as unknown as HTMLButtonElement | null; + expect(button?.disabled).toBe(false); + await act(async () => button?.click()); + expect(selected).toEqual([candidate]); + }); + + // 已在组织内的用户不可重复选择。 + test("已有成员候选按钮保持禁用", async () => { + const { MemberCandidateButton } = await import("../pages/agent-panel/pages/agent-organizations-dialogs"); + const container = win.document.createElement("div"); + win.document.body.appendChild(container); + root = createRoot(container as unknown as HTMLElement); + await act(async () => + root?.render( + + undefined} + /> + , + ), + ); + + const button = container.querySelector("button") as unknown as HTMLButtonElement | null; + expect(button?.disabled).toBe(true); + }); +}); diff --git a/web/src/pages/agent-panel/pages/agent-organizations-dialogs.tsx b/web/src/pages/agent-panel/pages/agent-organizations-dialogs.tsx index 50fc4f08..2d2c1784 100644 --- a/web/src/pages/agent-panel/pages/agent-organizations-dialogs.tsx +++ b/web/src/pages/agent-panel/pages/agent-organizations-dialogs.tsx @@ -1,4 +1,4 @@ -import { Check, Copy, X } from "lucide-react"; +import { Check, Copy, Search, X } from "lucide-react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { @@ -13,9 +13,9 @@ import { } from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; +import type { OrgMemberCandidate } from "@/src/api/organizations"; import type { MachineFormState, OrganizationsDialogsProps } from "./agent-organizations-types"; function Field({ label, children }: { label: string; children: React.ReactNode }) { @@ -64,6 +64,29 @@ function CreateOrganizationDialog({ props }: { props: OrganizationsDialogsProps ); } +export function MemberCandidateButton({ + candidate, + selected, + onAdd, +}: { + candidate: OrgMemberCandidate; + selected: boolean; + onAdd: (candidate: OrgMemberCandidate) => void; +}) { + const { t } = useTranslation("orgs"); + const disabled = candidate.isMember || selected; + return ( + + ); +} + function InviteMemberDialog({ props }: { props: OrganizationsDialogsProps }) { const { t } = useTranslation("orgs"); const showResults = props.debouncedInviteKeyword.length >= 3; @@ -74,8 +97,9 @@ function InviteMemberDialog({ props }: { props: OrganizationsDialogsProps }) { {t("inviteDialog.title")}
- - +
+ {t("inviteDialog.searchLabel")} +
{props.selectedCandidates.length > 0 ? (
{props.selectedCandidates.map((candidate) => ( @@ -92,55 +116,46 @@ function InviteMemberDialog({ props }: { props: OrganizationsDialogsProps }) { ))}
) : null} - 0 - ? t("inviteDialog.searchMorePlaceholder") - : t("inviteDialog.searchPlaceholder") - } - /> - +
+
+
{props.debouncedInviteKeyword.length === 0 ? (
{t("inviteDialog.searchHint")}
) : null} {props.debouncedInviteKeyword.length > 0 && props.debouncedInviteKeyword.length < 3 ? (
{t("inviteDialog.searchMinChars")}
) : null} - {showResults ? ( - - {props.memberCandidatesLoading ? t("inviteDialog.searching") : t("inviteDialog.empty")} - + {showResults && props.memberCandidatesLoading ? ( +
{t("inviteDialog.searching")}
) : null} - {props.memberCandidates.length > 0 ? ( - - {props.memberCandidates.map((candidate) => { - const selected = props.selectedCandidates.some((item) => item.id === candidate.id); - const disabled = candidate.isMember || selected; - return ( - !disabled && props.onCandidateAdd(candidate)} - > -
- {candidate.name} - {candidate.email} -
- {candidate.isMember ? ( - {t("inviteDialog.alreadyMember")} - ) : null} - {selected ? : null} -
- ); - })} -
+ {showResults && !props.memberCandidatesLoading && props.memberCandidates.length === 0 ? ( +
{t("inviteDialog.empty")}
) : null} - - - + {props.memberCandidates.map((candidate) => { + const selected = props.selectedCandidates.some((item) => item.id === candidate.id); + return ( + + ); + })} +
+
+