diff --git a/frontend/src/views/admin/SubscriptionsView.vue b/frontend/src/views/admin/SubscriptionsView.vue index d31578c9be..840ee477c8 100644 --- a/frontend/src/views/admin/SubscriptionsView.vue +++ b/frontend/src/views/admin/SubscriptionsView.vue @@ -1126,6 +1126,11 @@ const clearFilterUser = () => { // User search with debounce const debounceSearchUsers = () => { + // Invalidate the assignment target before the debounced search runs. + if (selectedUser.value && userSearchKeyword.value.trim() !== selectedUser.value.email) { + selectedUser.value = null + assignForm.user_id = null + } if (userSearchTimeout) { clearTimeout(userSearchTimeout) } @@ -1135,12 +1140,6 @@ const debounceSearchUsers = () => { const searchUsers = async () => { const keyword = userSearchKeyword.value.trim() - // Clear selection if user modified the search keyword - if (selectedUser.value && keyword !== selectedUser.value.email) { - selectedUser.value = null - assignForm.user_id = null - } - if (!keyword) { userSearchResults.value = [] return diff --git a/frontend/src/views/admin/__tests__/SubscriptionsView.userUsageLink.spec.ts b/frontend/src/views/admin/__tests__/SubscriptionsView.userUsageLink.spec.ts index d9b4ae68fe..7ae5e82da5 100644 --- a/frontend/src/views/admin/__tests__/SubscriptionsView.userUsageLink.spec.ts +++ b/frontend/src/views/admin/__tests__/SubscriptionsView.userUsageLink.spec.ts @@ -4,8 +4,10 @@ import { defineComponent } from 'vue' import SubscriptionsView from '../SubscriptionsView.vue' -const { listSubscriptions, getAllGroups, listUsers, searchUsageUsers } = vi.hoisted(() => ({ +const { listSubscriptions, assignSubscription, getAllGroups, listUsers, searchUsageUsers, showError } = vi.hoisted(() => ({ listSubscriptions: vi.fn(), + assignSubscription: vi.fn(), + showError: vi.fn(), getAllGroups: vi.fn(), listUsers: vi.fn(), searchUsageUsers: vi.fn() @@ -13,7 +15,7 @@ const { listSubscriptions, getAllGroups, listUsers, searchUsageUsers } = vi.hois vi.mock('@/api/admin', () => ({ adminAPI: { - subscriptions: { list: listSubscriptions }, + subscriptions: { list: listSubscriptions, assign: assignSubscription }, groups: { getAll: getAllGroups }, users: { list: listUsers }, usage: { searchUsers: searchUsageUsers } @@ -22,7 +24,7 @@ vi.mock('@/api/admin', () => ({ vi.mock('@/stores/app', () => ({ useAppStore: () => ({ - showError: vi.fn(), + showError, showSuccess: vi.fn() }) })) @@ -80,6 +82,7 @@ describe('admin subscription users', () => { total: 1, pages: 1 }) + assignSubscription.mockResolvedValue({}) getAllGroups.mockResolvedValue([]) listUsers.mockResolvedValue({ items: [{ id: 42, email: 'reader@example.com' }], @@ -143,6 +146,50 @@ describe('admin subscription users', () => { } }) + it.each(['another', ''])('clears the assignment user immediately when input changes to %j', async (keyword) => { + 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 form = wrapper.get('#assign-subscription-form') + form.getComponent({ name: 'Select' }).vm.$emit('update:modelValue', 3) + const search = wrapper.get('[data-assign-user-search] input') + await search.trigger('focus') + await search.setValue('reader') + await vi.advanceTimersByTimeAsync(300) + await flushPromises() + await wrapper.get('[data-assign-user-search] button').trigger('click') + + await search.setValue(keyword) + await form.trigger('submit') + await flushPromises() + + expect(assignSubscription).not.toHaveBeenCalled() + expect(showError).toHaveBeenCalledWith('admin.subscriptions.pleaseSelectUser') + expect(listUsers).toHaveBeenCalledTimes(1) + + listUsers.mockResolvedValue({ items: [{ id: 84, email: 'another@example.com' }] }) + await search.trigger('focus') + await search.setValue('another') + await vi.advanceTimersByTimeAsync(300) + await flushPromises() + await wrapper.get('[data-assign-user-search] button').trigger('click') + await form.trigger('submit') + await flushPromises() + + expect(assignSubscription).toHaveBeenCalledTimes(1) + expect(assignSubscription).toHaveBeenCalledWith({ + user_id: 84, group_id: 3, validity_days: 30 + }) + } finally { + wrapper.unmount() + vi.useRealTimers() + } + }) + it('keeps deleted users available when filtering subscription history', async () => { vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }) const wrapper = mountView()