forked from boos/BoosAPI
fix: add searchable token account selectors (#347)
This commit is contained in:
+24
-13
@@ -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([
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user