From 941a0487abc30ff9c66af5056244b25087d15b17 Mon Sep 17 00:00:00 2001 From: "chihao.ou" Date: Sat, 5 Sep 2026 19:55:52 +0800 Subject: [PATCH] fix(accounts): allow removing inactive group assignments --- .../components/account/EditAccountModal.vue | 14 +++++- .../__tests__/EditAccountModal.spec.ts | 50 ++++++++++++++++++- .../src/components/common/GroupSelector.vue | 6 +-- 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/account/EditAccountModal.vue b/frontend/src/components/account/EditAccountModal.vue index 6fe23eef0b..1615a78513 100644 --- a/frontend/src/components/account/EditAccountModal.vue +++ b/frontend/src/components/account/EditAccountModal.vue @@ -2851,7 +2851,7 @@ { + const groups = new Map(props.groups.map(group => [group.id, group])) + const assignedIds = new Set(props.account?.group_ids ?? []) + for (const group of props.account?.groups ?? []) { + if (assignedIds.has(group.id) && !groups.has(group.id)) { + groups.set(group.id, group) + } + } + return Array.from(groups.values()) +}) + // Spark 影子账号(parent_account_id 非空):代理恒继承母账号,不可独立编辑(外审 B/P1), // 故隐藏代理选择器。 const isSparkShadow = computed(() => props.account?.parent_account_id != null) diff --git a/frontend/src/components/account/__tests__/EditAccountModal.spec.ts b/frontend/src/components/account/__tests__/EditAccountModal.spec.ts index 5c85b7c2af..9f9b68a539 100644 --- a/frontend/src/components/account/__tests__/EditAccountModal.spec.ts +++ b/frontend/src/components/account/__tests__/EditAccountModal.spec.ts @@ -302,7 +302,7 @@ function buildOpenAIOAuthParentAccount() { } as any } -function mountModal(account = buildAccount()) { +function mountModal(account = buildAccount(), renderGroupSelector = false) { return mount(EditAccountModal, { props: { show: true, @@ -316,7 +316,7 @@ function mountModal(account = buildAccount()) { Select: SelectStub, Icon: true, ProxySelector: true, - GroupSelector: GroupSelectorStub, + GroupSelector: renderGroupSelector ? false : GroupSelectorStub, ModelWhitelistSelector: ModelWhitelistSelectorStub } } @@ -372,6 +372,52 @@ describe('EditAccountModal', () => { wrapper.unmount() }) + it('allows removing assigned inactive groups and undoing the selection before saving', async () => { + authIsSimpleMode.value = false + const account = buildAccount() + const activeGroup = { + id: 1, + name: 'Active group', + platform: 'openai', + status: 'active', + subscription_type: 'standard', + rate_multiplier: 1 + } + const inactiveGroup = { ...activeGroup, id: 2, name: 'Paused group', status: 'inactive' } + account.group_ids = [1, 2] + account.groups = [ + { ...activeGroup, name: 'Outdated name' }, + inactiveGroup, + inactiveGroup, + { ...inactiveGroup, id: 3, name: 'Unassigned paused group' } + ] + updateAccountMock.mockReset() + checkMixedChannelRiskMock.mockReset() + checkMixedChannelRiskMock.mockResolvedValue({ has_risk: false }) + updateAccountMock.mockResolvedValue(account) + + const wrapper = mountModal(account, true) + await wrapper.setProps({ groups: [activeGroup] as any }) + const selector = wrapper.get('[data-tour="account-form-groups"]') + expect(selector.findAll('input[type="checkbox"]').map(input => input.attributes('value'))) + .toEqual(['1', '2']) + expect(selector.text()).toContain('Active group') + expect(selector.text()).not.toContain('Outdated name') + const pausedCheckbox = selector.get('input[value="2"]') + expect(pausedCheckbox.element.checked).toBe(true) + + await pausedCheckbox.setValue(false) + expect(selector.get('input[value="2"]').element.checked).toBe(false) + await pausedCheckbox.setValue(true) + expect(pausedCheckbox.element.checked).toBe(true) + await pausedCheckbox.setValue(false) + await wrapper.get('form#edit-account-form').trigger('submit.prevent') + + expect(updateAccountMock).toHaveBeenCalledTimes(1) + expect(updateAccountMock.mock.calls[0]?.[1]?.group_ids).toEqual([1]) + expect(account.group_ids).toEqual([1, 2]) + }) + it('reopening the same account rehydrates the OpenAI whitelist from props', async () => { const account = buildAccount() updateAccountMock.mockReset() diff --git a/frontend/src/components/common/GroupSelector.vue b/frontend/src/components/common/GroupSelector.vue index fe2f372937..0cd01c21aa 100644 --- a/frontend/src/components/common/GroupSelector.vue +++ b/frontend/src/components/common/GroupSelector.vue @@ -61,7 +61,7 @@ import { computed, ref, watch } from 'vue' import { useI18n } from 'vue-i18n' import GroupBadge from './GroupBadge.vue' import Icon from '@/components/icons/Icon.vue' -import type { AdminGroup, GroupPlatform } from '@/types' +import type { Group, GroupPlatform } from '@/types' import { useAuthStore } from '@/stores' const { t } = useI18n() @@ -69,7 +69,7 @@ const authStore = useAuthStore() interface Props { modelValue: number[] - groups: AdminGroup[] + groups: (Group & { account_count?: number })[] platform?: GroupPlatform // Optional platform filter mixedScheduling?: boolean // For antigravity accounts: allow anthropic/gemini groups searchable?: boolean | 'auto' @@ -91,7 +91,7 @@ const isSearchable = computed(() => { // Filter groups by platform if specified const filteredGroups = computed(() => { - let result: AdminGroup[] = authStore.isSimpleMode + let result = authStore.isSimpleMode ? props.groups.filter((g) => g.platform !== 'composite') : props.groups if (props.platform) {