mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-02 16:32:14 +00:00
feat(admin): batch update user limits
This commit is contained in:
1 parent
09c6c6d740
commit
7947619cc3
31 files changed
+1243
-9
No files matched your search
@@ -242,6 +242,16 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="selectedCount > 0"
|
||||
class="btn btn-secondary flex-1 md:flex-initial"
|
||||
data-test="bulk-edit-limits"
|
||||
@click="showBulkEditModal = true"
|
||||
>
|
||||
<Icon name="users" size="md" class="mr-2" />
|
||||
{{ t('admin.users.bulkLimits.action', { count: selectedCount }) }}
|
||||
</button>
|
||||
|
||||
<!-- Create User Button (full width on mobile, auto width on desktop) -->
|
||||
<button @click="showCreateModal = true" class="btn btn-primary flex-1 md:flex-initial">
|
||||
<Icon name="plus" size="md" class="mr-2" />
|
||||
@@ -257,12 +267,17 @@
|
||||
:columns="columns"
|
||||
:data="sortedUsers"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
selectable
|
||||
:selected-keys="selectedIds"
|
||||
:selection-label="getUserSelectionLabel"
|
||||
:actions-count="7"
|
||||
:server-side-sort="true"
|
||||
default-sort-key="created_at"
|
||||
default-sort-order="desc"
|
||||
:sort-storage-key="USER_SORT_STORAGE_KEY"
|
||||
@sort="handleSort"
|
||||
@update:selected-keys="handleSelectedKeysUpdate"
|
||||
>
|
||||
<template #cell-email="{ value }">
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -735,6 +750,12 @@
|
||||
<ConfirmDialog :show="showDeleteDialog" :title="t('admin.users.deleteUser')" :message="t('admin.users.deleteConfirm', { email: deletingUser?.email })" :danger="true" @confirm="confirmDelete" @cancel="showDeleteDialog = false" />
|
||||
<UserCreateModal :show="showCreateModal" @close="showCreateModal = false" @success="loadUsers" />
|
||||
<UserEditModal :show="showEditModal" :user="editingUser" @close="closeEditModal" @success="loadUsers" />
|
||||
<BulkEditUserModal
|
||||
:show="showBulkEditModal"
|
||||
:selected-ids="selectedIds"
|
||||
@close="showBulkEditModal = false"
|
||||
@success="handleBulkLimitsSuccess"
|
||||
/>
|
||||
<UserPlatformQuotaModal
|
||||
:show="showPlatformQuotaModal"
|
||||
:user="platformQuotaUser"
|
||||
@@ -755,6 +776,7 @@ import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { getPersistedPageSize } from '@/composables/usePersistedPageSize'
|
||||
import { useTableSelection } from '@/composables/useTableSelection'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
|
||||
@@ -781,6 +803,7 @@ import PlatformCostCell from '@/components/user/PlatformCostCell.vue'
|
||||
import UserPlatformQuotaCell from '@/components/user/UserPlatformQuotaCell.vue'
|
||||
import UserCreateModal from '@/components/admin/user/UserCreateModal.vue'
|
||||
import UserEditModal from '@/components/admin/user/UserEditModal.vue'
|
||||
import BulkEditUserModal from '@/components/admin/user/BulkEditUserModal.vue'
|
||||
import UserPlatformQuotaModal from '@/components/admin/user/UserPlatformQuotaModal.vue'
|
||||
import UserApiKeysModal from '@/components/admin/user/UserApiKeysModal.vue'
|
||||
import UserAllowedGroupsModal from '@/components/admin/user/UserAllowedGroupsModal.vue'
|
||||
@@ -1268,6 +1291,23 @@ const sortedUsers = computed(() => {
|
||||
.map((x) => x.row)
|
||||
})
|
||||
|
||||
const {
|
||||
selectedIds,
|
||||
selectedCount,
|
||||
setSelectedIds,
|
||||
clear: clearSelection
|
||||
} = useTableSelection<AdminUser>({
|
||||
rows: sortedUsers,
|
||||
getId: (user) => user.id
|
||||
})
|
||||
|
||||
const handleSelectedKeysUpdate = (keys: Array<string | number>) => {
|
||||
setSelectedIds(keys.filter((key): key is number => typeof key === 'number'))
|
||||
}
|
||||
|
||||
const getUserSelectionLabel = (user: AdminUser) =>
|
||||
t('admin.users.bulkLimits.selectUser', { email: user.email })
|
||||
|
||||
// User attribute definitions and values
|
||||
const attributeDefinitions = ref<UserAttributeDefinition[]>([])
|
||||
const userAttributeValues = ref<Record<number, Record<number, string>>>({})
|
||||
@@ -1280,6 +1320,7 @@ const pagination = reactive({
|
||||
|
||||
const showCreateModal = ref(false)
|
||||
const showEditModal = ref(false)
|
||||
const showBulkEditModal = ref(false)
|
||||
const showDeleteDialog = ref(false)
|
||||
const showApiKeysModal = ref(false)
|
||||
const showAttributesModal = ref(false)
|
||||
@@ -1584,6 +1625,11 @@ const loadUsers = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleBulkLimitsSuccess = async () => {
|
||||
clearSelection()
|
||||
await loadUsers()
|
||||
}
|
||||
|
||||
let searchTimeout: ReturnType<typeof setTimeout>
|
||||
const handleSearch = () => {
|
||||
clearTimeout(searchTimeout)
|
||||
|
||||
@@ -77,13 +77,22 @@ const createAdminUser = (overrides: Partial<AdminUser> = {}): AdminUser => ({
|
||||
})
|
||||
|
||||
const DataTableStub = {
|
||||
props: ['columns', 'data'],
|
||||
emits: ['sort'],
|
||||
props: ['columns', 'data', 'selectedKeys'],
|
||||
emits: ['sort', 'update:selectedKeys'],
|
||||
template: `
|
||||
<div>
|
||||
<div data-test="columns">{{ columns.map(col => col.key).join(',') }}</div>
|
||||
<div data-test="row-order">{{ data.map(row => row.email).join(',') }}</div>
|
||||
<div data-test="selected-keys">{{ (selectedKeys || []).join(',') }}</div>
|
||||
<button data-test="sort-last-used" @click="$emit('sort', 'last_used_at', 'desc')">sort</button>
|
||||
<button
|
||||
v-for="row in data"
|
||||
:key="'select-' + row.id"
|
||||
:data-test="'select-' + row.id"
|
||||
@click="$emit('update:selectedKeys', Array.from(new Set([...(selectedKeys || []), row.id])))"
|
||||
>
|
||||
select
|
||||
</button>
|
||||
<template v-for="col in columns" :key="col.key">
|
||||
<slot :name="'header-' + col.key" :column="col" />
|
||||
</template>
|
||||
@@ -94,6 +103,22 @@ const DataTableStub = {
|
||||
`
|
||||
}
|
||||
|
||||
const PaginationStub = {
|
||||
emits: ['update:page'],
|
||||
template: '<button data-test="next-page" @click="$emit(\'update:page\', 2)">next</button>'
|
||||
}
|
||||
|
||||
const BulkEditUserModalStub = {
|
||||
props: ['show', 'selectedIds'],
|
||||
emits: ['close', 'success'],
|
||||
template: `
|
||||
<div v-if="show" data-test="bulk-modal">
|
||||
<span data-test="bulk-modal-ids">{{ selectedIds.join(',') }}</span>
|
||||
<button data-test="bulk-success" @click="$emit('success', selectedIds.length)">success</button>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
describe('admin UsersView', () => {
|
||||
beforeEach(() => {
|
||||
vi.useRealTimers()
|
||||
@@ -140,6 +165,8 @@ describe('admin UsersView', () => {
|
||||
UserConcurrencyCell: true,
|
||||
UserCreateModal: true,
|
||||
UserEditModal: true,
|
||||
BulkEditUserModal: BulkEditUserModalStub,
|
||||
UserPlatformQuotaModal: true,
|
||||
UserApiKeysModal: true,
|
||||
UserAllowedGroupsModal: true,
|
||||
UserBalanceModal: true,
|
||||
@@ -224,6 +251,8 @@ describe('admin UsersView', () => {
|
||||
UserConcurrencyCell: true,
|
||||
UserCreateModal: true,
|
||||
UserEditModal: true,
|
||||
BulkEditUserModal: BulkEditUserModalStub,
|
||||
UserPlatformQuotaModal: true,
|
||||
UserApiKeysModal: true,
|
||||
UserAllowedGroupsModal: true,
|
||||
UserBalanceModal: true,
|
||||
@@ -264,4 +293,80 @@ describe('admin UsersView', () => {
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps selected user IDs across pages and clears them after a successful bulk update', async () => {
|
||||
let refreshed = false
|
||||
listUsers.mockImplementation(async (page: number) => {
|
||||
const user = page === 2
|
||||
? createAdminUser({
|
||||
id: 43,
|
||||
email: refreshed ? 'refreshed-page-two@example.com' : 'page-two@example.com'
|
||||
})
|
||||
: createAdminUser({ id: 42, email: 'page-one@example.com' })
|
||||
return {
|
||||
items: [user],
|
||||
total: 2,
|
||||
page,
|
||||
page_size: 20,
|
||||
pages: 2
|
||||
}
|
||||
})
|
||||
|
||||
const wrapper = mount(UsersView, {
|
||||
global: {
|
||||
stubs: {
|
||||
AppLayout: { template: '<div><slot /></div>' },
|
||||
TablePageLayout: {
|
||||
template: '<div><slot name="filters" /><slot name="table" /><slot name="pagination" /></div>'
|
||||
},
|
||||
DataTable: DataTableStub,
|
||||
Pagination: PaginationStub,
|
||||
ConfirmDialog: true,
|
||||
EmptyState: true,
|
||||
GroupBadge: true,
|
||||
Select: true,
|
||||
UserAttributesConfigModal: true,
|
||||
UserConcurrencyCell: true,
|
||||
UserCreateModal: true,
|
||||
UserEditModal: true,
|
||||
BulkEditUserModal: BulkEditUserModalStub,
|
||||
UserPlatformQuotaModal: true,
|
||||
UserApiKeysModal: true,
|
||||
UserAllowedGroupsModal: true,
|
||||
UserBalanceModal: true,
|
||||
UserBalanceHistoryModal: true,
|
||||
GroupReplaceModal: true,
|
||||
Icon: true,
|
||||
Teleport: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.find('[data-test="bulk-edit-limits"]').exists()).toBe(false)
|
||||
await wrapper.get('[data-test="select-42"]').trigger('click')
|
||||
expect(wrapper.get('[data-test="selected-keys"]').text()).toBe('42')
|
||||
expect(wrapper.find('[data-test="bulk-edit-limits"]').exists()).toBe(true)
|
||||
|
||||
await wrapper.get('[data-test="next-page"]').trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.get('[data-test="selected-keys"]').text()).toBe('42')
|
||||
|
||||
await wrapper.get('[data-test="select-43"]').trigger('click')
|
||||
expect(wrapper.get('[data-test="selected-keys"]').text()).toBe('42,43')
|
||||
|
||||
await wrapper.get('[data-test="bulk-edit-limits"]').trigger('click')
|
||||
expect(wrapper.get('[data-test="bulk-modal-ids"]').text()).toBe('42,43')
|
||||
|
||||
const callsBeforeSuccess = listUsers.mock.calls.length
|
||||
refreshed = true
|
||||
await wrapper.get('[data-test="bulk-success"]').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(listUsers.mock.calls.length).toBeGreaterThan(callsBeforeSuccess)
|
||||
expect(wrapper.get('[data-test="row-order"]').text()).toBe('refreshed-page-two@example.com')
|
||||
expect(wrapper.find('[data-test="bulk-edit-limits"]').exists()).toBe(false)
|
||||
expect(wrapper.get('[data-test="selected-keys"]').text()).toBe('')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user