fix(subscriptions): exclude deleted users from assignment search

This commit is contained in:
chihao.ou
2026-09-09 20:01:34 +08:00
parent 98d86915be
commit c76db386c0
2 changed files with 83 additions and 11 deletions
@@ -769,7 +769,7 @@ import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app'
import { adminAPI } from '@/api/admin'
import type { UserSubscription, Group, GroupPlatform, SubscriptionType } from '@/types'
import type { AdminUser, UserSubscription, Group, GroupPlatform, SubscriptionType } from '@/types'
import type { SimpleUser } from '@/api/admin/usage'
import type { Column } from '@/components/common/types'
import { formatDateTimeToMinute } from '@/utils/format'
@@ -944,10 +944,10 @@ let filterUserSearchTimeout: ReturnType<typeof setTimeout> | null = null
// User search state
const userSearchKeyword = ref('')
const userSearchResults = ref<SimpleUser[]>([])
const userSearchResults = ref<AdminUser[]>([])
const userSearchLoading = ref(false)
const showUserDropdown = ref(false)
const selectedUser = ref<SimpleUser | null>(null)
const selectedUser = ref<AdminUser | null>(null)
let userSearchTimeout: ReturnType<typeof setTimeout> | null = null
const filters = reactive({
@@ -1148,7 +1148,10 @@ const searchUsers = async () => {
userSearchLoading.value = true
try {
userSearchResults.value = await adminAPI.usage.searchUsers(keyword)
const result = await adminAPI.users.list(1, 30, {
search: keyword, sort_by: 'email', sort_order: 'asc'
})
userSearchResults.value = result.items
} catch (error) {
console.error('Failed to search users:', error)
userSearchResults.value = []
@@ -1157,7 +1160,7 @@ const searchUsers = async () => {
}
}
const selectUser = (user: SimpleUser) => {
const selectUser = (user: AdminUser) => {
selectedUser.value = user
userSearchKeyword.value = user.email
showUserDropdown.value = false
@@ -4,15 +4,19 @@ import { defineComponent } from 'vue'
import SubscriptionsView from '../SubscriptionsView.vue'
const { listSubscriptions, getAllGroups } = vi.hoisted(() => ({
const { listSubscriptions, getAllGroups, listUsers, searchUsageUsers } = vi.hoisted(() => ({
listSubscriptions: vi.fn(),
getAllGroups: vi.fn()
getAllGroups: vi.fn(),
listUsers: vi.fn(),
searchUsageUsers: vi.fn()
}))
vi.mock('@/api/admin', () => ({
adminAPI: {
subscriptions: { list: listSubscriptions },
groups: { getAll: getAllGroups }
groups: { getAll: getAllGroups },
users: { list: listUsers },
usage: { searchUsers: searchUsageUsers }
}
}))
@@ -51,7 +55,7 @@ const RouterLinkStub = defineComponent({
template: '<a :href="`${to.path}?user_id=${to.query.user_id}`"><slot /></a>'
})
describe('admin subscription user usage link', () => {
describe('admin subscription users', () => {
beforeEach(() => {
vi.clearAllMocks()
localStorage.clear()
@@ -77,17 +81,28 @@ describe('admin subscription user usage link', () => {
pages: 1
})
getAllGroups.mockResolvedValue([])
listUsers.mockResolvedValue({
items: [{ id: 42, email: 'reader@example.com' }],
total: 1,
pages: 1
})
searchUsageUsers.mockResolvedValue([
{ id: 14, email: 'deleted@example.com', deleted: true }
])
})
const mountView = () => mount(SubscriptionsView, {
global: {
stubs: {
AppLayout: { template: '<div><slot /></div>' },
TablePageLayout: { template: '<div><slot name="table" /></div>' },
TablePageLayout: { template: '<div><slot name="filters" /><slot name="table" /></div>' },
DataTable: DataTableStub,
RouterLink: RouterLinkStub,
Pagination: true,
BaseDialog: true,
BaseDialog: {
props: ['show'],
template: '<div v-if="show"><slot /><slot name="footer" /></div>'
},
ConfirmDialog: true,
EmptyState: true,
Select: true,
@@ -99,6 +114,60 @@ describe('admin subscription user usage link', () => {
}
})
it('searches current users when assigning a subscription', async () => {
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
const wrapper = mountView()
try {
await flushPromises()
await wrapper.findAll('button')
.find((button) => button.text() === 'admin.subscriptions.assignSubscription')!
.trigger('click')
const search = wrapper.get('[data-assign-user-search] input')
await search.trigger('focus')
await search.setValue(' example.com ')
await vi.advanceTimersByTimeAsync(300)
await flushPromises()
expect(listUsers).toHaveBeenCalledWith(1, 30, {
search: 'example.com', sort_by: 'email', sort_order: 'asc'
})
expect(searchUsageUsers).not.toHaveBeenCalled()
const picker = wrapper.get('[data-assign-user-search]')
expect(picker.text()).toContain('reader@example.com')
expect(picker.text()).not.toContain('deleted@example.com')
await picker.get('button').trigger('click')
expect((search.element as HTMLInputElement).value).toBe('reader@example.com')
} finally {
wrapper.unmount()
vi.useRealTimers()
}
})
it('keeps deleted users available when filtering subscription history', async () => {
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
const wrapper = mountView()
try {
await flushPromises()
const search = wrapper.get('[data-filter-user-search] input')
await search.trigger('focus')
await search.setValue('deleted')
await vi.advanceTimersByTimeAsync(300)
await flushPromises()
expect(searchUsageUsers).toHaveBeenCalledWith('deleted')
expect(listUsers).not.toHaveBeenCalled()
const picker = wrapper.get('[data-filter-user-search]')
expect(picker.text()).toContain('deleted@example.com')
await picker.get('button').trigger('click')
expect(listSubscriptions).toHaveBeenLastCalledWith(
1, expect.any(Number), expect.objectContaining({ user_id: 14 }), expect.any(Object)
)
} finally {
wrapper.unmount()
vi.useRealTimers()
}
})
it('renders the user email as a link to that user filtered usage records', async () => {
const wrapper = mountView()