fix: add searchable token account selectors (#347)

This commit is contained in:
Cita
2026-03-31 19:13:07 +08:00
committed by GitHub
parent 6b19d06140
commit 87390651f4
2 changed files with 97 additions and 13 deletions
+24 -13
View File
@@ -58,6 +58,9 @@ type SyncableAccount = {
name?: string | null; name?: string | null;
} | null; } | null;
}; };
const ACCOUNT_SELECT_SEARCH_PLACEHOLDER = '筛选账号(名称 / 站点)';
const isAccountSyncable = (account: any) => const isAccountSyncable = (account: any) =>
resolveAccountCredentialMode(account) === 'session' resolveAccountCredentialMode(account) === 'session'
&& account?.status === 'active' && account?.status === 'active'
@@ -307,6 +310,17 @@ export function TokensPanel({ embedded = false, onEmbeddedActionsChange }: Token
&& accountClusteredTokens.every((token) => selectedTokenIds.includes(token.id)); && accountClusteredTokens.every((token) => selectedTokenIds.includes(token.id));
const activeAccounts = useMemo(() => accounts.filter(isAccountSyncable), [accounts]); const activeAccounts = useMemo(() => accounts.filter(isAccountSyncable), [accounts]);
const activeAccountSelectOptions = useMemo(() => (
activeAccounts.map((account) => {
const accountName = account.username || `account-${account.id}`;
const siteName = account.site?.name || '-';
return {
value: String(account.id),
label: `${accountName} @ ${siteName}`,
description: siteName,
};
})
), [activeAccounts]);
useEffect(() => { useEffect(() => {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
@@ -787,12 +801,11 @@ export function TokensPanel({ embedded = false, onEmbeddedActionsChange }: Token
onChange={(nextValue) => setSyncingAccountId(Number.parseInt(nextValue, 10) || 0)} onChange={(nextValue) => setSyncingAccountId(Number.parseInt(nextValue, 10) || 0)}
options={[ options={[
{ value: '0', label: '选择账号后同步站点令牌' }, { value: '0', label: '选择账号后同步站点令牌' },
...activeAccounts.map((account) => ({ ...activeAccountSelectOptions,
value: String(account.id),
label: `${account.username || `account-${account.id}`} @ ${account.site?.name || '-'}`,
})),
]} ]}
placeholder="选择账号后同步站点令牌" placeholder="选择账号后同步站点令牌"
searchable
searchPlaceholder={ACCOUNT_SELECT_SEARCH_PLACEHOLDER}
/> />
</div> </div>
<button <button
@@ -820,7 +833,7 @@ export function TokensPanel({ embedded = false, onEmbeddedActionsChange }: Token
{showAdd ? '取消' : '+ 新增令牌'} {showAdd ? '取消' : '+ 新增令牌'}
</button> </button>
</div> </div>
), [activeAccounts, allVisibleTokensSelected, embedded, handleSync, handleSyncAll, handleToggleAdd, isMobile, showAdd, syncing, syncingAccountId, syncingAll]); ), [activeAccountSelectOptions, activeAccounts.length, allVisibleTokensSelected, embedded, handleSync, handleSyncAll, handleToggleAdd, isMobile, showAdd, syncing, syncingAccountId, syncingAll]);
useEffect(() => { useEffect(() => {
if (!embedded || !onEmbeddedActionsChange) return; if (!embedded || !onEmbeddedActionsChange) return;
@@ -853,12 +866,11 @@ export function TokensPanel({ embedded = false, onEmbeddedActionsChange }: Token
onChange={(nextValue) => setSyncingAccountId(Number.parseInt(nextValue, 10) || 0)} onChange={(nextValue) => setSyncingAccountId(Number.parseInt(nextValue, 10) || 0)}
options={[ options={[
{ value: '0', label: '选择账号后同步站点令牌' }, { value: '0', label: '选择账号后同步站点令牌' },
...activeAccounts.map((account) => ({ ...activeAccountSelectOptions,
value: String(account.id),
label: `${account.username || `account-${account.id}`} @ ${account.site?.name || '-'}`,
})),
]} ]}
placeholder="选择账号后同步站点令牌" placeholder="选择账号后同步站点令牌"
searchable
searchPlaceholder={ACCOUNT_SELECT_SEARCH_PLACEHOLDER}
/> />
</div> </div>
<button <button
@@ -1055,12 +1067,11 @@ export function TokensPanel({ embedded = false, onEmbeddedActionsChange }: Token
}} }}
options={[ options={[
{ value: '0', label: '选择账号' }, { value: '0', label: '选择账号' },
...activeAccounts.map((account) => ({ ...activeAccountSelectOptions,
value: String(account.id),
label: `${account.username || `account-${account.id}`} @ ${account.site?.name || '-'}`,
})),
]} ]}
placeholder="选择账号" placeholder="选择账号"
searchable
searchPlaceholder={ACCOUNT_SELECT_SEARCH_PLACEHOLDER}
/> />
</div> </div>
{createHintModelName ? ( {createHintModelName ? (
@@ -1,6 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { act, create, type ReactTestInstance } from 'react-test-renderer'; import { act, create, type ReactTestInstance } from 'react-test-renderer';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import ModernSelect from '../components/ModernSelect.js';
import { ToastProvider } from '../components/Toast.js'; import { ToastProvider } from '../components/Toast.js';
import Accounts from './Accounts.js'; import Accounts from './Accounts.js';
import { TokensPanel } from './Tokens.js'; import { TokensPanel } from './Tokens.js';
@@ -257,6 +258,78 @@ describe('Tokens edit modal and row selection', () => {
} }
}); });
it('uses searchable account selectors for sync and add-token flows', async () => {
apiMock.getAccounts.mockResolvedValue([
{
id: 1,
username: 'session-user',
accessToken: 'session-token',
status: 'active',
credentialMode: 'session',
capabilities: { canCheckin: true, canRefreshBalance: true, proxyOnly: false },
site: { id: 10, name: 'Session Site', platform: 'new-api', status: 'active', url: 'https://session.example.com' },
},
{
id: 2,
username: 'codex-user',
accessToken: 'codex-token',
status: 'active',
credentialMode: 'session',
capabilities: { canCheckin: true, canRefreshBalance: true, proxyOnly: false },
site: { id: 11, name: 'Codex Workspace', platform: 'codex', status: 'active', url: 'https://workspace.example.com' },
},
]);
let root!: WebTestRenderer;
try {
await act(async () => {
root = buildTokensRoot();
});
await flushMicrotasks();
const syncAccountSelect = root.root.findAllByType(ModernSelect)
.find((node) => node.props.placeholder === '选择账号后同步站点令牌');
expect(syncAccountSelect).toBeTruthy();
expect(syncAccountSelect!.props.searchable).toBe(true);
expect(syncAccountSelect!.props.searchPlaceholder).toBe('筛选账号(名称 / 站点)');
expect(syncAccountSelect!.props.options).toEqual(
expect.arrayContaining([
expect.objectContaining({
value: '2',
label: 'codex-user @ Codex Workspace',
description: 'Codex Workspace',
}),
]),
);
const addButton = root.root.findAll((node) => node.type === 'button')
.find((node) => collectText(node).includes('+ 新增令牌'));
expect(addButton).toBeTruthy();
await act(async () => {
addButton!.props.onClick();
});
await flushMicrotasks();
const addAccountSelect = root.root.findAllByType(ModernSelect)
.find((node) => node.props.placeholder === '选择账号');
expect(addAccountSelect).toBeTruthy();
expect(addAccountSelect!.props.searchable).toBe(true);
expect(addAccountSelect!.props.searchPlaceholder).toBe('筛选账号(名称 / 站点)');
expect(addAccountSelect!.props.options).toEqual(
expect.arrayContaining([
expect.objectContaining({
value: '2',
label: 'codex-user @ Codex Workspace',
description: 'Codex Workspace',
}),
]),
);
} finally {
root?.unmount();
}
});
it('shows placeholder guidance and saves masked_pending tokens as ready values', async () => { it('shows placeholder guidance and saves masked_pending tokens as ready values', async () => {
apiMock.getAccountTokens.mockResolvedValue([ apiMock.getAccountTokens.mockResolvedValue([
{ {