From 0da18e4a5e4ea65180fd26e30b3539650e7fe58d Mon Sep 17 00:00:00 2001 From: William Drake Date: Sun, 27 Sep 2026 11:49:25 -0400 Subject: [PATCH 1/2] feat(admin): inline quick-adjust stepper for account priority Replace the static priority number in the accounts table with a compact stepper: hover reveals -/+ buttons, clicking the value allows typing (Enter to save, Esc to cancel, arrow keys to nudge). Rapid clicks are debounced into a single priority-only PUT, failures revert the value and surface a toast, and the row is patched in place without a full reload. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../account/AccountPriorityCell.vue | 182 ++++++++++++++++++ .../__tests__/AccountPriorityCell.spec.ts | 89 +++++++++ .../src/i18n/locales/en/admin/accounts.ts | 6 + .../src/i18n/locales/zh/admin/accounts.ts | 6 + frontend/src/views/admin/AccountsView.vue | 9 +- 5 files changed, 290 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/account/AccountPriorityCell.vue create mode 100644 frontend/src/components/account/__tests__/AccountPriorityCell.spec.ts diff --git a/frontend/src/components/account/AccountPriorityCell.vue b/frontend/src/components/account/AccountPriorityCell.vue new file mode 100644 index 0000000000..e9b84fb378 --- /dev/null +++ b/frontend/src/components/account/AccountPriorityCell.vue @@ -0,0 +1,182 @@ + + + diff --git a/frontend/src/components/account/__tests__/AccountPriorityCell.spec.ts b/frontend/src/components/account/__tests__/AccountPriorityCell.spec.ts new file mode 100644 index 0000000000..0656f2194e --- /dev/null +++ b/frontend/src/components/account/__tests__/AccountPriorityCell.spec.ts @@ -0,0 +1,89 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { flushPromises, mount } from '@vue/test-utils' +import AccountPriorityCell from '../AccountPriorityCell.vue' +import type { Account } from '@/types' +import { update } from '@/api/admin/accounts' + +vi.mock('@/api/admin/accounts', () => ({ update: vi.fn() })) +vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (key: string) => key }) })) + +const account = (overrides: Partial = {}) => ({ + id: 7, name: 'Claude 1', platform: 'anthropic', type: 'oauth', priority: 3, + ...overrides, +}) as Account + +const mountCell = (value = account()) => mount(AccountPriorityCell, { props: { account: value } }) + +beforeEach(() => { + vi.useFakeTimers() + vi.mocked(update).mockReset().mockImplementation(async (id, req) => account({ id, priority: req.priority })) +}) +afterEach(() => { + vi.useRealTimers() +}) + +describe('AccountPriorityCell', () => { + it('batches rapid +/- clicks into a single priority-only update', async () => { + const wrapper = mountCell() + await wrapper.get('[data-testid="account-priority-increment"]').trigger('click') + await wrapper.get('[data-testid="account-priority-increment"]').trigger('click') + await wrapper.get('[data-testid="account-priority-decrement"]').trigger('click') + expect(wrapper.get('[data-testid="account-priority-value"]').text()).toBe('4') + expect(update).not.toHaveBeenCalled() + + await vi.runAllTimersAsync() + await flushPromises() + + expect(update).toHaveBeenCalledTimes(1) + expect(update).toHaveBeenCalledWith(7, { priority: 4 }) + expect(wrapper.emitted('updated')?.[0]?.[0]).toMatchObject({ id: 7, priority: 4 }) + }) + + it('does not go below 1', async () => { + const wrapper = mountCell(account({ priority: 1 })) + const dec = wrapper.get('[data-testid="account-priority-decrement"]') + expect(dec.attributes('disabled')).toBeDefined() + await dec.trigger('click') + await vi.runAllTimersAsync() + expect(update).not.toHaveBeenCalled() + }) + + it('saves a typed value on Enter and ignores unchanged input', async () => { + const wrapper = mountCell() + await wrapper.get('[data-testid="account-priority-value"]').trigger('click') + const input = wrapper.get('[data-testid="account-priority-input"]') + await input.setValue('12') + await input.trigger('keydown', { key: 'Enter' }) + await flushPromises() + expect(update).toHaveBeenCalledWith(7, { priority: 12 }) + + vi.mocked(update).mockClear() + await wrapper.setProps({ account: account({ priority: 12 }) }) + await wrapper.get('[data-testid="account-priority-value"]').trigger('click') + await wrapper.get('[data-testid="account-priority-input"]').trigger('keydown', { key: 'Enter' }) + await flushPromises() + expect(update).not.toHaveBeenCalled() + }) + + it('Escape cancels typing without saving', async () => { + const wrapper = mountCell() + await wrapper.get('[data-testid="account-priority-value"]').trigger('click') + const input = wrapper.get('[data-testid="account-priority-input"]') + await input.setValue('50') + await input.trigger('keydown', { key: 'Escape' }) + await flushPromises() + expect(update).not.toHaveBeenCalled() + expect(wrapper.get('[data-testid="account-priority-value"]').text()).toBe('3') + }) + + it('reverts and emits an error when the update fails', async () => { + vi.mocked(update).mockRejectedValueOnce(new Error('boom')) + const wrapper = mountCell() + await wrapper.get('[data-testid="account-priority-increment"]').trigger('click') + await vi.runAllTimersAsync() + await flushPromises() + expect(wrapper.get('[data-testid="account-priority-value"]').text()).toBe('3') + expect(wrapper.emitted('error')).toHaveLength(1) + expect(wrapper.emitted('updated')).toBeUndefined() + }) +}) diff --git a/frontend/src/i18n/locales/en/admin/accounts.ts b/frontend/src/i18n/locales/en/admin/accounts.ts index 40fd7b6f0b..856b571e5b 100644 --- a/frontend/src/i18n/locales/en/admin/accounts.ts +++ b/frontend/src/i18n/locales/en/admin/accounts.ts @@ -96,6 +96,12 @@ export default { schedulableEnabled: 'Scheduling enabled', schedulableDisabled: 'Scheduling disabled', failedToToggleSchedulable: 'Failed to toggle scheduling status', + priorityQuick: { + raise: 'Raise priority (value -1)', + lower: 'Lower priority (value +1)', + editHint: 'Click to type a value; lower is used first', + failed: 'Failed to update priority' + }, groupCountTotal: '{count} groups total', platforms: { anthropic: 'Anthropic', diff --git a/frontend/src/i18n/locales/zh/admin/accounts.ts b/frontend/src/i18n/locales/zh/admin/accounts.ts index bc374be293..ba3b6becab 100644 --- a/frontend/src/i18n/locales/zh/admin/accounts.ts +++ b/frontend/src/i18n/locales/zh/admin/accounts.ts @@ -94,6 +94,12 @@ export default { schedulableEnabled: '调度已开启', schedulableDisabled: '调度已关闭', failedToToggleSchedulable: '切换调度状态失败', + priorityQuick: { + raise: '提高优先级(数值 -1)', + lower: '降低优先级(数值 +1)', + editHint: '点击直接输入;数值越小越优先', + failed: '更新优先级失败' + }, groupCountTotal: '共 {count} 个分组', columns: { name: '名称', diff --git a/frontend/src/views/admin/AccountsView.vue b/frontend/src/views/admin/AccountsView.vue index 5df2c8afaa..2502f2db8c 100644 --- a/frontend/src/views/admin/AccountsView.vue +++ b/frontend/src/views/admin/AccountsView.vue @@ -378,8 +378,12 @@ @probe="handleProbeUpstreamBilling(row)" /> -