Merge pull request #2030 from KnowSky404/feature/account-bulk-edit-scope-and-compact
feat: support filtered account bulk edit and align compact OpenAI bulk fields
This commit is contained in:
@@ -141,7 +141,17 @@
|
||||
</div>
|
||||
</template>
|
||||
<template #table>
|
||||
<AccountBulkActionsBar :selected-ids="selIds" @delete="handleBulkDelete" @reset-status="handleBulkResetStatus" @refresh-token="handleBulkRefreshToken" @edit="showBulkEdit = true" @clear="clearSelection" @select-page="selectPage" @toggle-schedulable="handleBulkToggleSchedulable" />
|
||||
<AccountBulkActionsBar
|
||||
:selected-ids="selIds"
|
||||
@delete="handleBulkDelete"
|
||||
@reset-status="handleBulkResetStatus"
|
||||
@refresh-token="handleBulkRefreshToken"
|
||||
@edit-selected="openBulkEditSelected"
|
||||
@edit-filtered="openBulkEditFiltered"
|
||||
@clear="clearSelection"
|
||||
@select-page="selectPage"
|
||||
@toggle-schedulable="handleBulkToggleSchedulable"
|
||||
/>
|
||||
<div ref="accountTableRef" class="flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<DataTable
|
||||
ref="dataTableRef"
|
||||
@@ -303,7 +313,17 @@
|
||||
<AccountActionMenu :show="menu.show" :account="menu.acc" :position="menu.pos" @close="menu.show = false" @test="handleTest" @stats="handleViewStats" @schedule="handleSchedule" @reauth="handleReAuth" @refresh-token="handleRefresh" @recover-state="handleRecoverState" @reset-quota="handleResetQuota" @set-privacy="handleSetPrivacy" />
|
||||
<SyncFromCrsModal :show="showSync" @close="showSync = false" @synced="reload" />
|
||||
<ImportDataModal :show="showImportData" @close="showImportData = false" @imported="handleDataImported" />
|
||||
<BulkEditAccountModal :show="showBulkEdit" :account-ids="selIds" :selected-platforms="selPlatforms" :selected-types="selTypes" :proxies="proxies" :groups="groups" @close="showBulkEdit = false" @updated="handleBulkUpdated" />
|
||||
<BulkEditAccountModal
|
||||
:show="showBulkEdit"
|
||||
:account-ids="selIds"
|
||||
:selected-platforms="selPlatforms"
|
||||
:selected-types="selTypes"
|
||||
:target="bulkEditTarget ?? undefined"
|
||||
:proxies="proxies"
|
||||
:groups="groups"
|
||||
@close="showBulkEdit = false"
|
||||
@updated="handleBulkUpdated"
|
||||
/>
|
||||
<TempUnschedStatusModal :show="showTempUnsched" :account="tempUnschedAcc" @close="showTempUnsched = false" @reset="handleTempUnschedReset" />
|
||||
<ConfirmDialog :show="showDeleteDialog" :title="t('admin.accounts.deleteAccount')" :message="t('admin.accounts.deleteConfirm', { name: deletingAcc?.name })" :confirm-text="t('common.delete')" :cancel-text="t('common.cancel')" :danger="true" @confirm="confirmDelete" @cancel="showDeleteDialog = false" />
|
||||
<ConfirmDialog :show="showExportDataDialog" :title="t('admin.accounts.dataExport')" :message="t('admin.accounts.dataExportConfirmMessage')" :confirm-text="t('admin.accounts.dataExportConfirm')" :cancel-text="t('common.cancel')" @confirm="handleExportData" @cancel="showExportDataDialog = false">
|
||||
@@ -364,6 +384,29 @@ const proxies = ref<AccountProxy[]>([])
|
||||
const groups = ref<AdminGroup[]>([])
|
||||
const accountTableRef = ref<HTMLElement | null>(null)
|
||||
const dataTableRef = ref<InstanceType<typeof DataTable> | null>(null)
|
||||
type AccountBulkEditTarget =
|
||||
| {
|
||||
mode: 'selected'
|
||||
accountIds: number[]
|
||||
selectedPlatforms: AccountPlatform[]
|
||||
selectedTypes: AccountType[]
|
||||
}
|
||||
| {
|
||||
mode: 'filtered'
|
||||
filters: {
|
||||
platform?: string
|
||||
type?: string
|
||||
status?: string
|
||||
group?: string
|
||||
search?: string
|
||||
privacy_mode?: string
|
||||
sort_by?: string
|
||||
sort_order?: AccountSortOrder
|
||||
}
|
||||
previewCount: number
|
||||
selectedPlatforms: AccountPlatform[]
|
||||
selectedTypes: AccountType[]
|
||||
}
|
||||
const selPlatforms = computed<AccountPlatform[]>(() => {
|
||||
const platforms = new Set(
|
||||
accounts.value
|
||||
@@ -387,6 +430,7 @@ const showImportData = ref(false)
|
||||
const showExportDataDialog = ref(false)
|
||||
const includeProxyOnExport = ref(true)
|
||||
const showBulkEdit = ref(false)
|
||||
const bulkEditTarget = ref<AccountBulkEditTarget | null>(null)
|
||||
const showTempUnsched = ref(false)
|
||||
const showDeleteDialog = ref(false)
|
||||
const showReAuth = ref(false)
|
||||
@@ -1216,7 +1260,57 @@ const handleBulkToggleSchedulable = async (schedulable: boolean) => {
|
||||
appStore.showError(t('common.error'))
|
||||
}
|
||||
}
|
||||
const handleBulkUpdated = () => { showBulkEdit.value = false; clearSelection(); reload() }
|
||||
const buildBulkEditFilterSnapshot = () => {
|
||||
const rawParams = toRaw(params) as Record<string, unknown>
|
||||
const sortOrder: AccountSortOrder = rawParams.sort_order === 'desc' ? 'desc' : 'asc'
|
||||
return {
|
||||
platform: typeof rawParams.platform === 'string' ? rawParams.platform : '',
|
||||
type: typeof rawParams.type === 'string' ? rawParams.type : '',
|
||||
status: typeof rawParams.status === 'string' ? rawParams.status : '',
|
||||
group: typeof rawParams.group === 'string' ? rawParams.group : '',
|
||||
search: typeof rawParams.search === 'string' ? rawParams.search : '',
|
||||
privacy_mode: typeof rawParams.privacy_mode === 'string' ? rawParams.privacy_mode : '',
|
||||
sort_by: typeof rawParams.sort_by === 'string' ? rawParams.sort_by : '',
|
||||
sort_order: sortOrder
|
||||
}
|
||||
}
|
||||
|
||||
const collectSelectionMetadata = (rows: Account[]) => {
|
||||
const selectedPlatforms = Array.from(new Set(rows.map(account => account.platform)))
|
||||
const selectedTypes = Array.from(new Set(rows.map(account => account.type)))
|
||||
return { selectedPlatforms, selectedTypes }
|
||||
}
|
||||
|
||||
const openBulkEditSelected = () => {
|
||||
bulkEditTarget.value = {
|
||||
mode: 'selected',
|
||||
accountIds: [...selIds.value],
|
||||
selectedPlatforms: [...selPlatforms.value],
|
||||
selectedTypes: [...selTypes.value]
|
||||
}
|
||||
showBulkEdit.value = true
|
||||
}
|
||||
|
||||
const openBulkEditFiltered = async () => {
|
||||
const filters = buildBulkEditFilterSnapshot()
|
||||
const preview = await adminAPI.accounts.list(1, 100, filters)
|
||||
const { selectedPlatforms, selectedTypes } = collectSelectionMetadata(preview.items)
|
||||
bulkEditTarget.value = {
|
||||
mode: 'filtered',
|
||||
filters,
|
||||
previewCount: preview.total,
|
||||
selectedPlatforms,
|
||||
selectedTypes
|
||||
}
|
||||
showBulkEdit.value = true
|
||||
}
|
||||
|
||||
const handleBulkUpdated = () => {
|
||||
showBulkEdit.value = false
|
||||
bulkEditTarget.value = null
|
||||
clearSelection()
|
||||
reload()
|
||||
}
|
||||
const handleDataImported = () => { showImportData.value = false; reload() }
|
||||
const ACCOUNT_UNGROUPED_GROUP_QUERY_VALUE = 'ungrouped'
|
||||
const ACCOUNT_PRIVACY_MODE_UNSET_QUERY_VALUE = '__unset__'
|
||||
|
||||
152
frontend/src/views/admin/__tests__/AccountsView.bulkEdit.spec.ts
Normal file
152
frontend/src/views/admin/__tests__/AccountsView.bulkEdit.spec.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
|
||||
import AccountsView from '../AccountsView.vue'
|
||||
|
||||
const {
|
||||
listAccounts,
|
||||
listWithEtag,
|
||||
getBatchTodayStats,
|
||||
getAllProxies,
|
||||
getAllGroups
|
||||
} = vi.hoisted(() => ({
|
||||
listAccounts: vi.fn(),
|
||||
listWithEtag: vi.fn(),
|
||||
getBatchTodayStats: vi.fn(),
|
||||
getAllProxies: vi.fn(),
|
||||
getAllGroups: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/api/admin', () => ({
|
||||
adminAPI: {
|
||||
accounts: {
|
||||
list: listAccounts,
|
||||
listWithEtag,
|
||||
getBatchTodayStats,
|
||||
delete: vi.fn(),
|
||||
batchClearError: vi.fn(),
|
||||
batchRefresh: vi.fn(),
|
||||
toggleSchedulable: vi.fn()
|
||||
},
|
||||
proxies: {
|
||||
getAll: getAllProxies
|
||||
},
|
||||
groups: {
|
||||
getAll: getAllGroups
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/app', () => ({
|
||||
useAppStore: () => ({
|
||||
showError: vi.fn(),
|
||||
showSuccess: vi.fn(),
|
||||
showInfo: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/auth', () => ({
|
||||
useAuthStore: () => ({
|
||||
token: 'test-token'
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('vue-i18n', async () => {
|
||||
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const DataTableStub = {
|
||||
props: ['columns', 'data'],
|
||||
template: '<div data-test="data-table"></div>'
|
||||
}
|
||||
|
||||
const AccountBulkActionsBarStub = {
|
||||
props: ['selectedIds'],
|
||||
emits: ['edit-filtered'],
|
||||
template: '<button data-test="edit-filtered" @click="$emit(\'edit-filtered\')">edit filtered</button>'
|
||||
}
|
||||
|
||||
const BulkEditAccountModalStub = {
|
||||
props: ['show', 'target'],
|
||||
template: '<div data-test="bulk-edit-modal" :data-show="String(show)" :data-target-mode="target?.mode ?? \'\'"></div>'
|
||||
}
|
||||
|
||||
describe('admin AccountsView bulk edit scope', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
|
||||
listAccounts.mockReset()
|
||||
listWithEtag.mockReset()
|
||||
getBatchTodayStats.mockReset()
|
||||
getAllProxies.mockReset()
|
||||
getAllGroups.mockReset()
|
||||
|
||||
listAccounts.mockResolvedValue({
|
||||
items: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
pages: 0
|
||||
})
|
||||
listWithEtag.mockResolvedValue({
|
||||
notModified: true,
|
||||
etag: null,
|
||||
data: null
|
||||
})
|
||||
getBatchTodayStats.mockResolvedValue({ stats: {} })
|
||||
getAllProxies.mockResolvedValue([])
|
||||
getAllGroups.mockResolvedValue([])
|
||||
})
|
||||
|
||||
it('opens bulk edit in filtered-results mode from the bulk actions dropdown', async () => {
|
||||
const wrapper = mount(AccountsView, {
|
||||
global: {
|
||||
stubs: {
|
||||
AppLayout: { template: '<div><slot /></div>' },
|
||||
TablePageLayout: {
|
||||
template: '<div><slot name="filters" /><slot name="table" /><slot name="pagination" /></div>'
|
||||
},
|
||||
DataTable: DataTableStub,
|
||||
Pagination: true,
|
||||
ConfirmDialog: true,
|
||||
AccountTableActions: { template: '<div><slot name="beforeCreate" /><slot name="after" /></div>' },
|
||||
AccountTableFilters: { template: '<div></div>' },
|
||||
AccountBulkActionsBar: AccountBulkActionsBarStub,
|
||||
AccountActionMenu: true,
|
||||
ImportDataModal: true,
|
||||
ReAuthAccountModal: true,
|
||||
AccountTestModal: true,
|
||||
AccountStatsModal: true,
|
||||
ScheduledTestsPanel: true,
|
||||
SyncFromCrsModal: true,
|
||||
TempUnschedStatusModal: true,
|
||||
ErrorPassthroughRulesModal: true,
|
||||
TLSFingerprintProfilesModal: true,
|
||||
CreateAccountModal: true,
|
||||
EditAccountModal: true,
|
||||
BulkEditAccountModal: BulkEditAccountModalStub,
|
||||
PlatformTypeBadge: true,
|
||||
AccountCapacityCell: true,
|
||||
AccountStatusIndicator: true,
|
||||
AccountTodayStatsCell: true,
|
||||
AccountGroupsCell: true,
|
||||
AccountUsageCell: true,
|
||||
Icon: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
await wrapper.get('[data-test="edit-filtered"]').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.get('[data-test="bulk-edit-modal"]').attributes('data-show')).toBe('true')
|
||||
expect(wrapper.get('[data-test="bulk-edit-modal"]').attributes('data-target-mode')).toBe('filtered')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user