fix: add searchable token account selectors (#347)
This commit is contained in:
+24
-13
@@ -58,6 +58,9 @@ type SyncableAccount = {
|
||||
name?: string | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
const ACCOUNT_SELECT_SEARCH_PLACEHOLDER = '筛选账号(名称 / 站点)';
|
||||
|
||||
const isAccountSyncable = (account: any) =>
|
||||
resolveAccountCredentialMode(account) === 'session'
|
||||
&& account?.status === 'active'
|
||||
@@ -307,6 +310,17 @@ export function TokensPanel({ embedded = false, onEmbeddedActionsChange }: Token
|
||||
&& accountClusteredTokens.every((token) => selectedTokenIds.includes(token.id));
|
||||
|
||||
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(() => {
|
||||
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)}
|
||||
options={[
|
||||
{ value: '0', label: '选择账号后同步站点令牌' },
|
||||
...activeAccounts.map((account) => ({
|
||||
value: String(account.id),
|
||||
label: `${account.username || `account-${account.id}`} @ ${account.site?.name || '-'}`,
|
||||
})),
|
||||
...activeAccountSelectOptions,
|
||||
]}
|
||||
placeholder="选择账号后同步站点令牌"
|
||||
searchable
|
||||
searchPlaceholder={ACCOUNT_SELECT_SEARCH_PLACEHOLDER}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
@@ -820,7 +833,7 @@ export function TokensPanel({ embedded = false, onEmbeddedActionsChange }: Token
|
||||
{showAdd ? '取消' : '+ 新增令牌'}
|
||||
</button>
|
||||
</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(() => {
|
||||
if (!embedded || !onEmbeddedActionsChange) return;
|
||||
@@ -853,12 +866,11 @@ export function TokensPanel({ embedded = false, onEmbeddedActionsChange }: Token
|
||||
onChange={(nextValue) => setSyncingAccountId(Number.parseInt(nextValue, 10) || 0)}
|
||||
options={[
|
||||
{ value: '0', label: '选择账号后同步站点令牌' },
|
||||
...activeAccounts.map((account) => ({
|
||||
value: String(account.id),
|
||||
label: `${account.username || `account-${account.id}`} @ ${account.site?.name || '-'}`,
|
||||
})),
|
||||
...activeAccountSelectOptions,
|
||||
]}
|
||||
placeholder="选择账号后同步站点令牌"
|
||||
searchable
|
||||
searchPlaceholder={ACCOUNT_SELECT_SEARCH_PLACEHOLDER}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
@@ -1055,12 +1067,11 @@ export function TokensPanel({ embedded = false, onEmbeddedActionsChange }: Token
|
||||
}}
|
||||
options={[
|
||||
{ value: '0', label: '选择账号' },
|
||||
...activeAccounts.map((account) => ({
|
||||
value: String(account.id),
|
||||
label: `${account.username || `account-${account.id}`} @ ${account.site?.name || '-'}`,
|
||||
})),
|
||||
...activeAccountSelectOptions,
|
||||
]}
|
||||
placeholder="选择账号"
|
||||
searchable
|
||||
searchPlaceholder={ACCOUNT_SELECT_SEARCH_PLACEHOLDER}
|
||||
/>
|
||||
</div>
|
||||
{createHintModelName ? (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { act, create, type ReactTestInstance } from 'react-test-renderer';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import ModernSelect from '../components/ModernSelect.js';
|
||||
import { ToastProvider } from '../components/Toast.js';
|
||||
import Accounts from './Accounts.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 () => {
|
||||
apiMock.getAccountTokens.mockResolvedValue([
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user