diff --git a/openspec/changes/add-contacts-management-ui/design.md b/openspec/changes/add-contacts-management-ui/design.md
index 226f9bdee20..a3d5a6f469d 100644
--- a/openspec/changes/add-contacts-management-ui/design.md
+++ b/openspec/changes/add-contacts-management-ui/design.md
@@ -166,13 +166,14 @@ mock context 提供 `can_view_contacts`、`can_manage_contacts` 和 `can_manage_
### 10. Overlay、文案和测试遵循 web 规范
- Dialog、drawer、popover、toast 等使用 `@langgenius/dify-ui/*`。
-- 用户可见文案进入 `web/i18n/en-US/`,并同步所有受支持 locale。
+- 用户可见文案进入 `web/i18n/en-US/` 与 `web/i18n/zh-Hans/`;其他 locale 的
+ `contacts` namespace 按现有加载策略回退到英文。
- 测试优先覆盖可观察行为与回归风险,使用 Vitest、Testing Library 和确定性 repository。
- 关键场景覆盖键盘操作、焦点恢复、错误关联、状态通知和窄屏布局。
## Risks / Trade-offs
-- [Figma 节点细节尚未通过授权访问核对] → 实施第一步建立逐节点 acceptance matrix,再冻结 overlay 类型、字段、列、文案和 responsive 行为。
+- [后续 Figma 更新造成实现漂移] → 以 `figma-acceptance.md` 记录的十二个节点验收基线为准,并在后续设计变更中显式更新矩阵。
- [Mock view model 与未来 API 漂移] → 组件依赖稳定 UI view model,由未来 adapter 负责 DTO 映射。
- [Contacts 与 Members 出现重复状态] → repository composition 层提供单一 mock scenario,并在 member mutation 后精确刷新两个 feature 的 query。
- [Mock 权限被误认为安全边界] → 使用 feature gate,并在交付说明和测试命名中明确其仅为展示状态。
@@ -193,9 +194,12 @@ mock context 提供 `can_view_contacts`、`can_manage_contacts` 和 `can_manage_
后端能力完成后另建 change:冻结正式 contract、实现 API repository adapter、启用服务端权限与冲突校验,并替换 mock composition。
-## Open Questions
+## Resolved Figma Questions
-- 各 Figma 节点最终使用独立页面、drawer 还是 dialog,需要在实施前通过授权设计访问确认。
-- 列表列、详情 action 和 External contact 附加字段的最终集合,需要以 Figma acceptance matrix 为准。
-- EE 的 `Keep as Platform contact` 控件默认值和影响说明文案,需要按设计节点确认。
-- Contact 详情中的 channel summary 是否包含 workspace IM override 操作;若设计包含该操作,应另建 capability,不在本 change 中顺带实现。
+- 逐节点验收结果记录在 `figma-acceptance.md`。详情采用列表右侧 320px panel,
+ 创建与成员移除采用 dialog。
+- 列表展示名称、Email、类型、只读 channel summary 与加入时间;详情不提供没有
+ typed action 的编辑、删除、合并或 IM override 操作。
+- EE 的 `Keep as Platform contact` 默认勾选;CE / SaaS 不显示该控件。
+- `1649:8221` 的 workspace IM override 属于 `add-im-platform-binding-ui`,仅作为跨
+ change 参考,不进入本 repository 或 UI。
diff --git a/openspec/changes/add-contacts-management-ui/figma-acceptance.md b/openspec/changes/add-contacts-management-ui/figma-acceptance.md
new file mode 100644
index 00000000000..455edaa974b
--- /dev/null
+++ b/openspec/changes/add-contacts-management-ui/figma-acceptance.md
@@ -0,0 +1,48 @@
+# Contacts Management Figma Acceptance Matrix
+
+The Figma file is named Agent Roster, but every surface below belongs to the
+Contacts frontend domain. Agent Roster and workspace Members remain separate
+owners; Members composes only the removal-impact confirmation.
+
+| Node | Surface | Acceptance baseline |
+| ------------ | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `1294:64487` | Directory | Full-page Contacts route with title, help link, All / Workspace / External filters, search, Add external contact, and rows for name, email, type, channel summary, and joined date. |
+| `1282:62739` | Directory with details | Keep the current directory visible and open a 320px right detail panel. Search, filter, and pagination context remain unchanged while the panel is open. Bulk removal controls shown in the design are outside this change. |
+| `1459:32284` | Contact details | Gradient identity header, avatar, name, email, kind badge, channel summaries, and joined metadata. The panel is read-only in this change. |
+| `1515:3382` | Contact details menu | The design exposes edit/remove affordances. They are intentionally omitted because this change has no typed actions for editing, deleting, merging, or overriding IM identities. |
+| `1303:66983` | External contact dialog | 480px dialog with title, close control, identity preview, display name, required email, Cancel, and Add actions. |
+| `1303:67192` | External contact filled state | Preserve entered name/email while validation or a recoverable mutation result is shown. |
+| `1303:67388` | External contact ready state | Disable duplicate submission while pending; success closes the dialog, clears the draft, refreshes the active directory query, and restores focus to the trigger. |
+| `1649:8221` | Bind IM identity | Cross-change reference only. Workspace IM overrides belong to `add-im-platform-binding-ui` and are not implemented or stored by this repository. |
+| `1459:31142` | EE directory | Add Platform filter and an Add contact menu with Add from Platform and External contact. Enterprise-wide candidates never appear in the ordinary directory until explicitly added. |
+| `1459:32562` | Organization picker | Search Organization members, exclude existing contacts, keep explicit selection, and add one or more candidates as Platform contacts. The approved OpenSpec multi-select behavior takes precedence over the inspected single-select visual variant. |
+| `1515:3696` | EE member removal | One 480px confirmation dialog with member identity, impact warning, and a `Keep as Platform contact` checkbox. The checkbox defaults to selected. |
+| `1649:5297` | CE / SaaS member removal | One 480px confirmation dialog with member identity and a non-optional warning that the workspace contact is also removed. No retention control is shown. |
+
+## Responsive and accessibility baseline
+
+- The directory uses horizontal scrolling for its tabular columns and stacks
+ toolbar actions on narrow widths without hiding search or primary actions.
+- The detail panel uses the available viewport width up to 320px.
+- Dialogs fit the viewport with 16px outer margins and scroll their content
+ when necessary.
+- All interactive rows and controls have names, visible focus, keyboard access,
+ associated field errors, announced result/error states, and overlay focus
+ restoration.
+
+## Mount and composition boundaries
+
+- Route: `/contacts`, mounted under the common console layout and visible only
+ when the Contacts preview gate is enabled and the mock context allows viewing.
+- Deployment: CE, SaaS, and EE share the page shell. Only EE mounts the
+ Organization picker, Platform filter, and member-retention choice.
+- Permission context: `canViewContacts`, `canManageContacts`, and
+ `canManageMembers` are UI-only mock capabilities derived at the composition
+ root; they are not an authorization boundary.
+- State: directory/details/external-contact/member-impact use one Contacts-owned
+ typed mock repository. IM platform management keeps its existing independent
+ repository; the two features share only workspace/deployment navigation
+ context.
+- Gate fallback: when the gate is disabled, `/contacts` and its navigation entry
+ are unavailable and Members continues to call the existing production member
+ removal flow.
diff --git a/openspec/changes/add-contacts-management-ui/tasks.md b/openspec/changes/add-contacts-management-ui/tasks.md
index 1279d87ca1e..e3f99c8bb1c 100644
--- a/openspec/changes/add-contacts-management-ui/tasks.md
+++ b/openspec/changes/add-contacts-management-ui/tasks.md
@@ -1,48 +1,48 @@
## 1. Design Baseline and Frontend Boundaries
-- [ ] 1.1 Inspect all twelve referenced Figma nodes with authorized access and record an acceptance matrix for surfaces, fields, columns, actions, copy, responsive behavior, and the EE removal-retention default.
-- [ ] 1.2 Identify the Contacts route and CE / SaaS versus EE mount points, define the shared deployment and permission context, and confirm the UI remains separate from Agent Roster and workspace Members.
-- [ ] 1.3 Define the feature gate and composition boundary that enables mock-backed Contacts without changing the existing production member removal path when the gate is off.
-- [ ] 1.4 Map shared shell integration with `add-im-platform-binding-ui` so directory UI and IM integration UI reuse navigation context without sharing repository state.
+- [x] 1.1 Inspect all twelve referenced Figma nodes with authorized access and record an acceptance matrix for surfaces, fields, columns, actions, copy, responsive behavior, and the EE removal-retention default.
+- [x] 1.2 Identify the Contacts route and CE / SaaS versus EE mount points, define the shared deployment and permission context, and confirm the UI remains separate from Agent Roster and workspace Members.
+- [x] 1.3 Define the feature gate and composition boundary that enables mock-backed Contacts without changing the existing production member removal path when the gate is off.
+- [x] 1.4 Map shared shell integration with `add-im-platform-binding-ui` so directory UI and IM integration UI reuse navigation context without sharing repository state.
## 2. Typed Mock Repository
-- [ ] 2.1 Write failing unit tests for Contact discriminated unions, scenario consistency, pagination deduplication, External contact conflict classification, and member-removal state transitions.
-- [ ] 2.2 Add Contacts-owned TypeScript view models, query types, command types, typed mutation results, repository interface, and query keys for directory, details, Organization candidates, External contacts, and member removal.
-- [ ] 2.3 Add deterministic named scenarios covering CE, SaaS, EE, all permission levels, empty and paginated directories, load failures, three Contact types, External contact conflicts, Organization search, and every removal outcome.
-- [ ] 2.4 Implement the in-memory mock repository with controllable delays, stable identifiers, lower-case full-Email matching, consistent list/detail state, and no random behavior.
-- [ ] 2.5 Add the feature provider and React Query hooks that inject the repository, preserve precise cache boundaries, and issue no backend, generated-client, or Organization directory requests.
+- [x] 2.1 Write failing unit tests for Contact discriminated unions, scenario consistency, pagination deduplication, External contact conflict classification, and member-removal state transitions.
+- [x] 2.2 Add Contacts-owned TypeScript view models, query types, command types, typed mutation results, repository interface, and query keys for directory, details, Organization candidates, External contacts, and member removal.
+- [x] 2.3 Add deterministic named scenarios covering CE, SaaS, EE, all permission levels, empty and paginated directories, load failures, three Contact types, External contact conflicts, Organization search, and every removal outcome.
+- [x] 2.4 Implement the in-memory mock repository with controllable delays, stable identifiers, lower-case full-Email matching, consistent list/detail state, and no random behavior.
+- [x] 2.5 Add the feature provider and React Query hooks that inject the repository, preserve precise cache boundaries, and issue no backend, generated-client, or Organization directory requests.
## 3. Contacts Directory and EE Platform Contacts
-- [ ] 3.1 Write failing component tests for route visibility, view/manage permission variants, loading/error/empty/no-result states, the three Contact types, and CE / SaaS versus EE directory semantics.
-- [ ] 3.2 Implement the Figma-aligned Contacts page shell, list rows, type and status presentation, search, filters, pagination or incremental loading, retries, and URL-backed browsing context.
-- [ ] 3.3 Write failing tests for EE Organization candidate search, existing-Contact exclusion, multi-select, pending duplicate prevention, success, and recoverable failure.
-- [ ] 3.4 Implement the EE-only Organization picker and mock add-Platform-contact flow while keeping enterprise-wide candidates out of the normal Contacts list.
+- [x] 3.1 Write failing component tests for route visibility, view/manage permission variants, loading/error/empty/no-result states, the three Contact types, and CE / SaaS versus EE directory semantics.
+- [x] 3.2 Implement the Figma-aligned Contacts page shell, list rows, type and status presentation, search, filters, pagination or incremental loading, retries, and URL-backed browsing context.
+- [x] 3.3 Write failing tests for EE Organization candidate search, existing-Contact exclusion, multi-select, pending duplicate prevention, success, and recoverable failure.
+- [x] 3.4 Implement the EE-only Organization picker and mock add-Platform-contact flow while keeping enterprise-wide candidates out of the normal Contacts list.
## 4. Contact Details
-- [ ] 4.1 Write failing component tests for `contact_id` restoration, common identity fields, workspace/platform/external type-specific sections, read-only permissions, missing values, load failure, and not-found states.
-- [ ] 4.2 Implement the Figma-aligned Contact detail page, drawer, or dialog selected by the acceptance matrix, including safe channel summaries and no out-of-scope management actions.
-- [ ] 4.3 Add tests and implementation for preserving list search/filter/pagination on return, refreshing list/detail together after mutations, and converting an EE removed member from workspace contact to Platform contact without changing its stable identity.
+- [x] 4.1 Write failing component tests for `contact_id` restoration, common identity fields, workspace/platform/external type-specific sections, read-only permissions, missing values, load failure, and not-found states.
+- [x] 4.2 Implement the Figma-aligned Contact detail page, drawer, or dialog selected by the acceptance matrix, including safe channel summaries and no out-of-scope management actions.
+- [x] 4.3 Add tests and implementation for preserving list search/filter/pagination on return, refreshing list/detail together after mutations, and converting an EE removed member from workspace contact to Platform contact without changing its stable identity.
## 5. External Contact Creation
-- [ ] 5.1 Write failing component tests for entry permissions, required fields, Email format, duplicate External contact, workspace-contact match, Platform-contact match, pending state, mutation failure, success, cancel, and focus restoration.
-- [ ] 5.2 Implement the Figma-aligned External contact creation surface with local typed form state, accessible validation, conflict-specific feedback, duplicate-submit prevention, and mock repository submission.
-- [ ] 5.3 Implement success handling that refreshes the active directory query, clears the draft, and follows the acceptance matrix for closing the overlay or opening the new Contact detail.
+- [x] 5.1 Write failing component tests for entry permissions, required fields, Email format, duplicate External contact, workspace-contact match, Platform-contact match, pending state, mutation failure, success, cancel, and focus restoration.
+- [x] 5.2 Implement the Figma-aligned External contact creation surface with local typed form state, accessible validation, conflict-specific feedback, duplicate-submit prevention, and mock repository submission.
+- [x] 5.3 Implement success handling that refreshes the active directory query, clears the draft, and follows the acceptance matrix for closing the overlay or opening the new Contact detail.
## 6. Member Removal Contact Retention
-- [ ] 6.1 Write failing tests for protected members, pending invitation cancellation, CE / SaaS Contact removal warnings, both EE retention choices, duplicate confirmation prevention, failure recovery, and feature-gate fallback.
-- [ ] 6.2 Replace or extend the active-member confirmation path with one Contacts-aware dify-ui dialog while preserving the existing simple cancellation flow for pending invitations.
-- [ ] 6.3 Implement mock removal commands and targeted cache refresh so CE / SaaS removes the Contact, EE either removes or converts it to Platform contact, and failures leave member and Contact views unchanged.
-- [ ] 6.4 Add a regression test proving the mock-enabled path never calls `deleteMemberOrCancelInvitation` or any real member / Contact endpoint.
+- [x] 6.1 Write failing tests for protected members, pending invitation cancellation, CE / SaaS Contact removal warnings, both EE retention choices, duplicate confirmation prevention, failure recovery, and feature-gate fallback.
+- [x] 6.2 Replace or extend the active-member confirmation path with one Contacts-aware dify-ui dialog while preserving the existing simple cancellation flow for pending invitations.
+- [x] 6.3 Implement mock removal commands and targeted cache refresh so CE / SaaS removes the Contact, EE either removes or converts it to Platform contact, and failures leave member and Contact views unchanged.
+- [x] 6.4 Add a regression test proving the mock-enabled path never calls `deleteMemberOrCancelInvitation` or any real member / Contact endpoint.
## 7. Product Quality and Verification
-- [ ] 7.1 Add all Contacts user-facing copy to `web/i18n/en-US/` and update every supported locale with correct localized values.
-- [ ] 7.2 Match the Figma acceptance matrix using dify-ui tokens and primitives, including narrow layouts, empty/error states, keyboard navigation, visible focus, field error associations, live result updates, and overlay focus restoration.
-- [ ] 7.3 Run targeted Vitest and Testing Library suites and fix failures, including React Query cache isolation and fake-timer cleanup between mock scenarios.
-- [ ] 7.4 Run the repository-prescribed frontend formatting, lint, and type-check commands and resolve all issues introduced by the change.
-- [ ] 7.5 Audit the final diff to confirm it contains only frontend and OpenSpec changes, adds no backend/OpenAPI/generated-client code, makes no real Contact or member mutations, and leaves API adapter work to a later change.
+- [x] 7.1 Add all Contacts user-facing copy to `web/i18n/en-US/` and `web/i18n/zh-Hans/`; keep the existing English fallback for other locales.
+- [x] 7.2 Match the Figma acceptance matrix using dify-ui tokens and primitives, including narrow layouts, empty/error states, keyboard navigation, visible focus, field error associations, live result updates, and overlay focus restoration.
+- [x] 7.3 Run targeted Vitest and Testing Library suites and fix failures, including React Query cache isolation and fake-timer cleanup between mock scenarios.
+- [x] 7.4 Run the repository-prescribed frontend formatting, lint, and type-check commands and resolve all issues introduced by the change.
+- [x] 7.5 Audit the final diff to confirm it contains only frontend and OpenSpec changes, adds no backend/OpenAPI/generated-client code, makes no real Contact or member mutations, and leaves API adapter work to a later change.
diff --git a/web/app/(commonLayout)/contacts/page.tsx b/web/app/(commonLayout)/contacts/page.tsx
new file mode 100644
index 00000000000..03edc06f8ef
--- /dev/null
+++ b/web/app/(commonLayout)/contacts/page.tsx
@@ -0,0 +1,8 @@
+import { ContactsDirectoryPage } from '@/features/contacts/management/directory-page'
+import { isContactsManagementEnabled } from '@/features/contacts/management/feature-flag'
+import { notFound } from '@/next/navigation'
+
+export default function ContactsPage() {
+ if (!isContactsManagementEnabled()) notFound()
+ return
+}
diff --git a/web/app/(commonLayout)/providers.tsx b/web/app/(commonLayout)/providers.tsx
index 92d856ca088..f136f90eee8 100644
--- a/web/app/(commonLayout)/providers.tsx
+++ b/web/app/(commonLayout)/providers.tsx
@@ -6,6 +6,7 @@ import { OAuthRegistrationAnalytics } from '@/app/components/oauth-registration-
import { EventEmitterContextProvider } from '@/context/event-emitter-provider'
import { ModalContextProvider } from '@/context/modal-context-provider'
import { ProviderContextProvider } from '@/context/provider-context-provider'
+import { ContactsManagementRuntimeProvider } from '@/features/contacts/management/composition'
import { ConsoleBootstrapGate } from './console-bootstrap-gate'
import { ExternalServiceSync } from './external-service-sync'
import { CommonLayoutHydrationBoundary } from './hydration-boundary'
@@ -31,7 +32,9 @@ export function ConsoleContextProviders({ children }: { children: ReactNode }) {
return (
- {children}
+
+ {children}
+
)
diff --git a/web/app/components/header/account-setting/members-page/__tests__/member-menu.spec.tsx b/web/app/components/header/account-setting/members-page/__tests__/member-menu.spec.tsx
index b0160dc7530..2a338537709 100644
--- a/web/app/components/header/account-setting/members-page/__tests__/member-menu.spec.tsx
+++ b/web/app/components/header/account-setting/members-page/__tests__/member-menu.spec.tsx
@@ -5,12 +5,19 @@ import { QueryClient } from '@tanstack/react-query'
import { screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
+import { ContactsManagementMockProvider } from '@/features/contacts/management/composition'
+import {
+ ContactsMockScenario,
+ createContactsMockScenario,
+} from '@/features/contacts/management/mock/scenarios'
import { useUpdateRolesOfMember } from '@/service/access-control/use-member-roles'
import { useWorkspaceRoleList } from '@/service/access-control/use-workspace-roles'
import { deleteMemberOrCancelInvitation } from '@/service/common'
import { commonQueryKeys } from '@/service/use-common'
import MemberMenu from '../member-menu'
+const mockIsContactsManagementEnabled = vi.hoisted(() => vi.fn(() => true))
+
vi.mock('@/service/access-control/use-member-roles')
vi.mock('@/service/access-control/use-workspace-roles')
vi.mock('@/service/common', () => ({
@@ -21,6 +28,9 @@ vi.mock('@langgenius/dify-ui/toast', () => ({
success: vi.fn(),
},
}))
+vi.mock('@/features/contacts/management/feature-flag', () => ({
+ isContactsManagementEnabled: () => mockIsContactsManagementEnabled(),
+}))
const createRole = (overrides: Partial): Role => ({
id: 'role-1',
@@ -72,6 +82,7 @@ describe('MemberMenu', () => {
beforeEach(() => {
vi.clearAllMocks()
+ mockIsContactsManagementEnabled.mockReturnValue(true)
mockUpdateRolesOfMember.mockResolvedValue(undefined)
vi.mocked(deleteMemberOrCancelInvitation).mockResolvedValue({ result: 'success' })
vi.mocked(useUpdateRolesOfMember).mockReturnValue({
@@ -150,6 +161,17 @@ describe('MemberMenu', () => {
)
})
+ it.each([
+ ['workspace owner', { ...member, role: 'owner' as const }, false],
+ ['current member', member, true],
+ ])('keeps the existing protection for the %s', (_case, protectedMember, isCurrentUser) => {
+ renderWithSystemFeatures()
+
+ expect(
+ screen.queryByRole('button', { name: /members\.memberActions/i }),
+ ).not.toBeInTheDocument()
+ })
+
it('should require confirmation before removing a member', async () => {
const user = userEvent.setup()
const queryClient = createQueryClient()
@@ -179,4 +201,76 @@ describe('MemberMenu', () => {
expect(queryClient.getQueryState(membersQueryKey)?.isInvalidated).toBe(true)
expect(toast.success).toHaveBeenCalledWith('common.actionMsg.modifiedSuccessfully')
})
+
+ it('uses the mock Contacts-aware flow for an active member without calling the real service', async () => {
+ const user = userEvent.setup()
+ const scenario = createContactsMockScenario(ContactsMockScenario.EeMixed)
+ const queryClient = createQueryClient()
+ const membersQueryKey = [...commonQueryKeys.members, 'en-US']
+ const activeMember = { ...member, id: 'member-owner' }
+ queryClient.setQueryData(membersQueryKey, { accounts: [activeMember] })
+
+ renderWithSystemFeatures(
+
+
+ ,
+ { queryClient },
+ )
+
+ await user.click(screen.getByRole('button', { name: /members\.memberActions/i }))
+ await user.click(screen.getByRole('menuitem', { name: /members\.removeFromTeam/i }))
+
+ const dialog = screen.getByRole('alertdialog', {
+ name: /contacts\.memberRemoval\.title/,
+ })
+ expect(within(dialog).getByRole('checkbox', { name: /keepPlatform/ })).toBeChecked()
+ await user.click(within(dialog).getByRole('button', { name: 'contacts.memberRemoval.remove' }))
+
+ await waitFor(() => expect(dialog).not.toBeInTheDocument())
+ expect(deleteMemberOrCancelInvitation).not.toHaveBeenCalled()
+ expect(queryClient.getQueryData<{ accounts: Member[] }>(membersQueryKey)?.accounts).toEqual([])
+ expect(toast.success).toHaveBeenCalledWith('common.actionMsg.modifiedSuccessfully')
+ })
+
+ it('keeps the existing invitation cancellation path when Contacts mock is enabled', async () => {
+ const user = userEvent.setup()
+ const scenario = createContactsMockScenario(ContactsMockScenario.EeMixed)
+
+ renderWithSystemFeatures(
+
+
+ ,
+ )
+
+ await user.click(screen.getByRole('button', { name: /members\.memberActions/i }))
+ await user.click(screen.getByRole('menuitem', { name: /members\.removeFromTeam/i }))
+ const dialog = screen.getByRole('alertdialog', {
+ name: /common\.members\.removeFromTeamConfirmTitle/,
+ })
+ expect(within(dialog).queryByRole('checkbox')).not.toBeInTheDocument()
+ await user.click(within(dialog).getByRole('button', { name: /common\.operation\.confirm/i }))
+
+ await waitFor(() => expect(deleteMemberOrCancelInvitation).toHaveBeenCalledOnce())
+ })
+
+ it('keeps the production removal path when the Contacts feature gate is disabled', async () => {
+ const user = userEvent.setup()
+ const scenario = createContactsMockScenario(ContactsMockScenario.EeMixed)
+ mockIsContactsManagementEnabled.mockReturnValue(false)
+
+ renderWithSystemFeatures(
+
+
+ ,
+ )
+
+ await user.click(screen.getByRole('button', { name: /members\.memberActions/i }))
+ await user.click(screen.getByRole('menuitem', { name: /members\.removeFromTeam/i }))
+ const dialog = screen.getByRole('alertdialog', {
+ name: /common\.members\.removeFromTeamConfirmTitle/,
+ })
+ await user.click(within(dialog).getByRole('button', { name: /common\.operation\.confirm/i }))
+
+ await waitFor(() => expect(deleteMemberOrCancelInvitation).toHaveBeenCalledOnce())
+ })
})
diff --git a/web/app/components/header/account-setting/members-page/member-menu.tsx b/web/app/components/header/account-setting/members-page/member-menu.tsx
index 7f96f17c492..310475304a3 100644
--- a/web/app/components/header/account-setting/members-page/member-menu.tsx
+++ b/web/app/components/header/account-setting/members-page/member-menu.tsx
@@ -22,6 +22,9 @@ import { useQueryClient } from '@tanstack/react-query'
import { memo, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
+import { useOptionalContactsManagement } from '@/features/contacts/management/composition-context'
+import { isContactsManagementEnabled } from '@/features/contacts/management/feature-flag'
+import { MemberRemovalContactImpactDialog } from '@/features/contacts/management/member-removal-dialog'
import { useUpdateRolesOfMember } from '@/service/access-control/use-member-roles'
import { deleteMemberOrCancelInvitation } from '@/service/common'
import { commonQueryKeys } from '@/service/use-common'
@@ -35,6 +38,10 @@ type MemberMenuProps = {
onTransferOwnership?: () => void
}
+type MembersCache = {
+ accounts: Member[] | null
+}
+
const MemberMenu = ({
member,
isCurrentUser,
@@ -48,11 +55,18 @@ const MemberMenu = ({
const [assignModalOpen, setAssignModalOpen] = useState(false)
const [removeConfirmOpen, setRemoveConfirmOpen] = useState(false)
const [removing, setRemoving] = useState(false)
+ const contactsManagement = useOptionalContactsManagement()
const isOwner = member.role === 'owner'
const canAssignRoles = !isOwner && !isCurrentUser
const canRemove = !isOwner && !isCurrentUser
const showTransferOwnership = isOwner && canTransferOwnership
+ const useContactsAwareRemoval = Boolean(
+ isContactsManagementEnabled() &&
+ member.status !== 'pending' &&
+ contactsManagement.context &&
+ contactsManagement.repository,
+ )
const selectedRoles = member.roles || []
const memberName = member.name || member.email
@@ -106,6 +120,20 @@ const MemberMenu = ({
}
}, [member.id, queryClient, t])
+ const handleContactsRemovalSuccess = useCallback(() => {
+ queryClient.setQueriesData(
+ { queryKey: commonQueryKeys.members },
+ (cachedMembers) => {
+ if (!cachedMembers?.accounts) return cachedMembers
+ return {
+ ...cachedMembers,
+ accounts: cachedMembers.accounts.filter((account) => account.id !== member.id),
+ }
+ },
+ )
+ toast.success(t(($) => $['actionMsg.modifiedSuccessfully'], { ns: 'common' }))
+ }, [member.id, queryClient, t])
+
const handleTransferOwnership = useCallback(() => {
setOpen(false)
onTransferOwnership?.()
@@ -163,29 +191,38 @@ const MemberMenu = ({
)}
- !open && setRemoveConfirmOpen(false)}
- >
-
-