forked from boos/BoosAPI
refactor: share responsive batch actions across admin pages
This commit is contained in:
@@ -0,0 +1,16 @@
|
|||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
function readPageSource(relativePath: string): string {
|
||||||
|
return readFileSync(resolve(process.cwd(), relativePath), 'utf8').replace(/\r\n/g, '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ResponsiveBatchActionBar page adoption', () => {
|
||||||
|
it('is used by the repeated admin list pages instead of open-coded mobile and desktop batch wrappers', () => {
|
||||||
|
expect(readPageSource('src/web/pages/Accounts.tsx')).toContain('ResponsiveBatchActionBar');
|
||||||
|
expect(readPageSource('src/web/pages/Sites.tsx')).toContain('ResponsiveBatchActionBar');
|
||||||
|
expect(readPageSource('src/web/pages/Tokens.tsx')).toContain('ResponsiveBatchActionBar');
|
||||||
|
expect(readPageSource('src/web/pages/DownstreamKeys.tsx')).toContain('ResponsiveBatchActionBar');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { create } from 'react-test-renderer';
|
||||||
|
import ResponsiveBatchActionBar from './ResponsiveBatchActionBar.js';
|
||||||
|
|
||||||
|
describe('ResponsiveBatchActionBar', () => {
|
||||||
|
it('renders the shared mobile batch bar on mobile', () => {
|
||||||
|
const root = create(
|
||||||
|
<ResponsiveBatchActionBar isMobile info="已选 2 项">
|
||||||
|
<button type="button">批量启用</button>
|
||||||
|
</ResponsiveBatchActionBar>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const bar = root.root.find((node) => node.props.className === 'mobile-actions-bar mobile-batch-bar');
|
||||||
|
expect(bar).toBeTruthy();
|
||||||
|
expect(root.root.findByType('button').children).toContain('批量启用');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the desktop card wrapper on desktop', () => {
|
||||||
|
const root = create(
|
||||||
|
<ResponsiveBatchActionBar isMobile={false} info="已选 3 项">
|
||||||
|
<button type="button">批量删除</button>
|
||||||
|
</ResponsiveBatchActionBar>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const card = root.root.find((node) => node.props.className === 'card');
|
||||||
|
expect(card).toBeTruthy();
|
||||||
|
expect(root.root.findAllByType('span').some((node) => node.children.includes('已选 3 项'))).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import MobileBatchBar from './MobileBatchBar.js';
|
||||||
|
|
||||||
|
type ResponsiveBatchActionBarProps = {
|
||||||
|
isMobile: boolean;
|
||||||
|
info: React.ReactNode;
|
||||||
|
children: React.ReactNode;
|
||||||
|
desktopStyle?: React.CSSProperties;
|
||||||
|
infoStyle?: React.CSSProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_DESKTOP_STYLE: React.CSSProperties = {
|
||||||
|
padding: 12,
|
||||||
|
display: 'flex',
|
||||||
|
gap: 8,
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
alignItems: 'center',
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_INFO_STYLE: React.CSSProperties = {
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: 600,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ResponsiveBatchActionBar({
|
||||||
|
isMobile,
|
||||||
|
info,
|
||||||
|
children,
|
||||||
|
desktopStyle,
|
||||||
|
infoStyle,
|
||||||
|
}: ResponsiveBatchActionBarProps) {
|
||||||
|
if (isMobile) {
|
||||||
|
return <MobileBatchBar info={info}>{children}</MobileBatchBar>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card" style={{ ...DEFAULT_DESKTOP_STYLE, ...desktopStyle }}>
|
||||||
|
<span style={{ ...DEFAULT_INFO_STYLE, ...infoStyle }}>{info}</span>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,9 +2,9 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { api } from '../api.js';
|
import { api } from '../api.js';
|
||||||
import CenteredModal from '../components/CenteredModal.js';
|
import CenteredModal from '../components/CenteredModal.js';
|
||||||
import MobileBatchBar from '../components/MobileBatchBar.js';
|
|
||||||
import MobileFilterSheet from '../components/MobileFilterSheet.js';
|
import MobileFilterSheet from '../components/MobileFilterSheet.js';
|
||||||
import ResponsiveFormGrid from '../components/ResponsiveFormGrid.js';
|
import ResponsiveFormGrid from '../components/ResponsiveFormGrid.js';
|
||||||
|
import ResponsiveBatchActionBar from '../components/ResponsiveBatchActionBar.js';
|
||||||
import { useToast } from '../components/Toast.js';
|
import { useToast } from '../components/Toast.js';
|
||||||
import ModernSelect from '../components/ModernSelect.js';
|
import ModernSelect from '../components/ModernSelect.js';
|
||||||
import { MobileCard, MobileField } from '../components/MobileCard.js';
|
import { MobileCard, MobileField } from '../components/MobileCard.js';
|
||||||
@@ -1066,9 +1066,12 @@ export default function Accounts() {
|
|||||||
: <>确定要删除选中的 <strong>{deleteConfirm?.count || 0}</strong> 个连接吗?</>}
|
: <>确定要删除选中的 <strong>{deleteConfirm?.count || 0}</strong> 个连接吗?</>}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!isMobile && activeSegment !== 'tokens' && selectedAccountIds.length > 0 && (
|
{activeSegment !== 'tokens' && selectedAccountIds.length > 0 && (
|
||||||
<div className="card" style={{ padding: 12, marginBottom: 12, display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
|
<ResponsiveBatchActionBar
|
||||||
<span style={{ fontSize: 13, fontWeight: 600 }}>已选 {selectedAccountIds.length} 项</span>
|
isMobile={isMobile}
|
||||||
|
info={`已选 ${selectedAccountIds.length} 项`}
|
||||||
|
desktopStyle={{ marginBottom: 12 }}
|
||||||
|
>
|
||||||
<button data-testid="accounts-batch-refresh-balance" onClick={() => runBatchAccountAction('refreshBalance')} disabled={batchActionLoading} className="btn btn-ghost" style={{ border: '1px solid var(--color-border)' }}>
|
<button data-testid="accounts-batch-refresh-balance" onClick={() => runBatchAccountAction('refreshBalance')} disabled={batchActionLoading} className="btn btn-ghost" style={{ border: '1px solid var(--color-border)' }}>
|
||||||
批量刷新余额
|
批量刷新余额
|
||||||
</button>
|
</button>
|
||||||
@@ -1081,24 +1084,7 @@ export default function Accounts() {
|
|||||||
<button onClick={() => runBatchAccountAction('delete')} disabled={batchActionLoading} className="btn btn-link btn-link-danger">
|
<button onClick={() => runBatchAccountAction('delete')} disabled={batchActionLoading} className="btn btn-link btn-link-danger">
|
||||||
批量删除
|
批量删除
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</ResponsiveBatchActionBar>
|
||||||
)}
|
|
||||||
|
|
||||||
{isMobile && activeSegment !== 'tokens' && selectedAccountIds.length > 0 && (
|
|
||||||
<MobileBatchBar info={`已选 ${selectedAccountIds.length} 项`}>
|
|
||||||
<button data-testid="accounts-batch-refresh-balance" onClick={() => runBatchAccountAction('refreshBalance')} disabled={batchActionLoading} className="btn btn-ghost" style={{ border: '1px solid var(--color-border)' }}>
|
|
||||||
批量刷新余额
|
|
||||||
</button>
|
|
||||||
<button onClick={() => runBatchAccountAction('enable')} disabled={batchActionLoading} className="btn btn-ghost" style={{ border: '1px solid var(--color-border)' }}>
|
|
||||||
批量启用
|
|
||||||
</button>
|
|
||||||
<button onClick={() => runBatchAccountAction('disable')} disabled={batchActionLoading} className="btn btn-ghost" style={{ border: '1px solid var(--color-border)' }}>
|
|
||||||
批量禁用
|
|
||||||
</button>
|
|
||||||
<button onClick={() => runBatchAccountAction('delete')} disabled={batchActionLoading} className="btn btn-link btn-link-danger">
|
|
||||||
批量删除
|
|
||||||
</button>
|
|
||||||
</MobileBatchBar>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeSegment === 'tokens' ? (
|
{activeSegment === 'tokens' ? (
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { createPortal } from 'react-dom';
|
|||||||
import { api } from '../api.js';
|
import { api } from '../api.js';
|
||||||
import CenteredModal from '../components/CenteredModal.js';
|
import CenteredModal from '../components/CenteredModal.js';
|
||||||
import DeleteConfirmModal from '../components/DeleteConfirmModal.js';
|
import DeleteConfirmModal from '../components/DeleteConfirmModal.js';
|
||||||
import MobileBatchBar from '../components/MobileBatchBar.js';
|
|
||||||
import { MobileCard, MobileField } from '../components/MobileCard.js';
|
import { MobileCard, MobileField } from '../components/MobileCard.js';
|
||||||
import MobileFilterSheet from '../components/MobileFilterSheet.js';
|
import MobileFilterSheet from '../components/MobileFilterSheet.js';
|
||||||
|
import ResponsiveBatchActionBar from '../components/ResponsiveBatchActionBar.js';
|
||||||
import { useToast } from '../components/Toast.js';
|
import { useToast } from '../components/Toast.js';
|
||||||
import ModernSelect from '../components/ModernSelect.js';
|
import ModernSelect from '../components/ModernSelect.js';
|
||||||
import { useAnimatedVisibility } from '../components/useAnimatedVisibility.js';
|
import { useAnimatedVisibility } from '../components/useAnimatedVisibility.js';
|
||||||
@@ -1687,24 +1687,17 @@ export default function DownstreamKeys() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedIds.length > 0 ? (
|
{selectedIds.length > 0 ? (
|
||||||
isMobile ? (
|
<ResponsiveBatchActionBar
|
||||||
<MobileBatchBar info={`已选 ${selectedIds.length} 个密钥`}>
|
isMobile={isMobile}
|
||||||
<button className="btn btn-ghost" style={{ border: '1px solid var(--color-border)' }} onClick={openBatchMetadata} disabled={batchActionLoading}>归类/标签</button>
|
info={`已选 ${selectedIds.length} 个密钥`}
|
||||||
<button className="btn btn-ghost" style={{ border: '1px solid var(--color-border)' }} onClick={() => void batchRun('批量启用', selectedIds)} disabled={batchActionLoading}>启用</button>
|
infoStyle={{ color: 'var(--color-text-primary)' }}
|
||||||
<button className="btn btn-ghost" style={{ border: '1px solid var(--color-border)' }} onClick={() => void batchRun('批量禁用', selectedIds)} disabled={batchActionLoading}>禁用</button>
|
>
|
||||||
<button className="btn btn-ghost" style={{ border: '1px solid var(--color-border)' }} onClick={() => void batchRun('批量清零用量', selectedIds)} disabled={batchActionLoading}>清零</button>
|
<button className="btn btn-ghost" style={{ border: '1px solid var(--color-border)' }} onClick={openBatchMetadata} disabled={batchActionLoading}>{isMobile ? '归类/标签' : '批量归类/标签'}</button>
|
||||||
<button className="btn btn-link btn-link-danger" onClick={() => setDeleteConfirm({ mode: 'batch', ids: [...selectedIds] })} disabled={batchActionLoading}>删除</button>
|
<button className="btn btn-ghost" style={{ border: '1px solid var(--color-border)' }} onClick={() => void batchRun('批量启用', selectedIds)} disabled={batchActionLoading}>{isMobile ? '启用' : '批量启用'}</button>
|
||||||
</MobileBatchBar>
|
<button className="btn btn-ghost" style={{ border: '1px solid var(--color-border)' }} onClick={() => void batchRun('批量禁用', selectedIds)} disabled={batchActionLoading}>{isMobile ? '禁用' : '批量禁用'}</button>
|
||||||
) : (
|
<button className="btn btn-ghost" style={{ border: '1px solid var(--color-border)' }} onClick={() => void batchRun('批量清零用量', selectedIds)} disabled={batchActionLoading}>{isMobile ? '清零' : '批量清零用量'}</button>
|
||||||
<div className="card" style={{ padding: 12, display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
|
<button className="btn btn-link btn-link-danger" onClick={() => setDeleteConfirm({ mode: 'batch', ids: [...selectedIds] })} disabled={batchActionLoading}>{isMobile ? '删除' : '批量删除'}</button>
|
||||||
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--color-text-primary)' }}>已选 {selectedIds.length} 个密钥</span>
|
</ResponsiveBatchActionBar>
|
||||||
<button className="btn btn-ghost" style={{ border: '1px solid var(--color-border)' }} onClick={openBatchMetadata} disabled={batchActionLoading}>批量归类/标签</button>
|
|
||||||
<button className="btn btn-ghost" style={{ border: '1px solid var(--color-border)' }} onClick={() => void batchRun('批量启用', selectedIds)} disabled={batchActionLoading}>批量启用</button>
|
|
||||||
<button className="btn btn-ghost" style={{ border: '1px solid var(--color-border)' }} onClick={() => void batchRun('批量禁用', selectedIds)} disabled={batchActionLoading}>批量禁用</button>
|
|
||||||
<button className="btn btn-ghost" style={{ border: '1px solid var(--color-border)' }} onClick={() => void batchRun('批量清零用量', selectedIds)} disabled={batchActionLoading}>批量清零用量</button>
|
|
||||||
<button className="btn btn-link btn-link-danger" onClick={() => setDeleteConfirm({ mode: 'batch', ids: [...selectedIds] })} disabled={batchActionLoading}>批量删除</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="card" style={{ padding: 12, display: 'flex', flexDirection: 'column', gap: 12 }}>
|
<div className="card" style={{ padding: 12, display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||||
|
|||||||
+8
-36
@@ -2,8 +2,8 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
|||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { api } from '../api.js';
|
import { api } from '../api.js';
|
||||||
import CenteredModal from '../components/CenteredModal.js';
|
import CenteredModal from '../components/CenteredModal.js';
|
||||||
import MobileBatchBar from '../components/MobileBatchBar.js';
|
|
||||||
import MobileFilterSheet from '../components/MobileFilterSheet.js';
|
import MobileFilterSheet from '../components/MobileFilterSheet.js';
|
||||||
|
import ResponsiveBatchActionBar from '../components/ResponsiveBatchActionBar.js';
|
||||||
import { useToast } from '../components/Toast.js';
|
import { useToast } from '../components/Toast.js';
|
||||||
import ModernSelect from '../components/ModernSelect.js';
|
import ModernSelect from '../components/ModernSelect.js';
|
||||||
import { MobileCard, MobileField } from '../components/MobileCard.js';
|
import { MobileCard, MobileField } from '../components/MobileCard.js';
|
||||||
@@ -689,9 +689,12 @@ export default function Sites() {
|
|||||||
</div>
|
</div>
|
||||||
</MobileFilterSheet>
|
</MobileFilterSheet>
|
||||||
|
|
||||||
{!isMobile && selectedSiteIds.length > 0 && (
|
{selectedSiteIds.length > 0 && (
|
||||||
<div className="card" style={{ padding: 12, marginBottom: 12, display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
|
<ResponsiveBatchActionBar
|
||||||
<span style={{ fontSize: 13, fontWeight: 600 }}>已选 {selectedSiteIds.length} 项</span>
|
isMobile={isMobile}
|
||||||
|
info={`已选 ${selectedSiteIds.length} 项`}
|
||||||
|
desktopStyle={{ marginBottom: 12 }}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
data-testid="sites-batch-enable-system-proxy"
|
data-testid="sites-batch-enable-system-proxy"
|
||||||
onClick={() => runBatchAction('enableSystemProxy')}
|
onClick={() => runBatchAction('enableSystemProxy')}
|
||||||
@@ -718,38 +721,7 @@ export default function Sites() {
|
|||||||
<button onClick={() => runBatchAction('delete')} disabled={batchActionLoading} className="btn btn-link btn-link-danger">
|
<button onClick={() => runBatchAction('delete')} disabled={batchActionLoading} className="btn btn-link btn-link-danger">
|
||||||
批量删除
|
批量删除
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</ResponsiveBatchActionBar>
|
||||||
)}
|
|
||||||
|
|
||||||
{isMobile && selectedSiteIds.length > 0 && (
|
|
||||||
<MobileBatchBar info={`已选 ${selectedSiteIds.length} 项`}>
|
|
||||||
<button
|
|
||||||
data-testid="sites-batch-enable-system-proxy"
|
|
||||||
onClick={() => runBatchAction('enableSystemProxy')}
|
|
||||||
disabled={batchActionLoading}
|
|
||||||
className="btn btn-ghost"
|
|
||||||
style={{ border: '1px solid var(--color-border)' }}
|
|
||||||
>
|
|
||||||
批量开启系统代理
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => runBatchAction('disableSystemProxy')}
|
|
||||||
disabled={batchActionLoading}
|
|
||||||
className="btn btn-ghost"
|
|
||||||
style={{ border: '1px solid var(--color-border)' }}
|
|
||||||
>
|
|
||||||
批量关闭系统代理
|
|
||||||
</button>
|
|
||||||
<button onClick={() => runBatchAction('enable')} disabled={batchActionLoading} className="btn btn-ghost" style={{ border: '1px solid var(--color-border)' }}>
|
|
||||||
批量启用
|
|
||||||
</button>
|
|
||||||
<button onClick={() => runBatchAction('disable')} disabled={batchActionLoading} className="btn btn-ghost" style={{ border: '1px solid var(--color-border)' }}>
|
|
||||||
批量禁用
|
|
||||||
</button>
|
|
||||||
<button onClick={() => runBatchAction('delete')} disabled={batchActionLoading} className="btn btn-link btn-link-danger">
|
|
||||||
批量删除
|
|
||||||
</button>
|
|
||||||
</MobileBatchBar>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="info-tip" style={{ marginBottom: 12 }}>
|
<div className="info-tip" style={{ marginBottom: 12 }}>
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|||||||
import { Navigate, useLocation, useNavigate } from 'react-router-dom';
|
import { Navigate, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { api } from '../api.js';
|
import { api } from '../api.js';
|
||||||
import CenteredModal from '../components/CenteredModal.js';
|
import CenteredModal from '../components/CenteredModal.js';
|
||||||
import MobileBatchBar from '../components/MobileBatchBar.js';
|
|
||||||
import MobileFilterSheet from '../components/MobileFilterSheet.js';
|
import MobileFilterSheet from '../components/MobileFilterSheet.js';
|
||||||
import ResponsiveFormGrid from '../components/ResponsiveFormGrid.js';
|
import ResponsiveFormGrid from '../components/ResponsiveFormGrid.js';
|
||||||
|
import ResponsiveBatchActionBar from '../components/ResponsiveBatchActionBar.js';
|
||||||
import { useToast } from '../components/Toast.js';
|
import { useToast } from '../components/Toast.js';
|
||||||
import { formatDateTimeLocal } from './helpers/checkinLogTime.js';
|
import { formatDateTimeLocal } from './helpers/checkinLogTime.js';
|
||||||
import {
|
import {
|
||||||
@@ -996,9 +996,12 @@ export function TokensPanel({ embedded = false, onEmbeddedActionsChange }: Token
|
|||||||
) : null}
|
) : null}
|
||||||
</CenteredModal>
|
</CenteredModal>
|
||||||
|
|
||||||
{!isMobile && selectedTokenIds.length > 0 && (
|
{selectedTokenIds.length > 0 && (
|
||||||
<div className="card" style={{ padding: 12, marginBottom: 12, display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
|
<ResponsiveBatchActionBar
|
||||||
<span style={{ fontSize: 13, fontWeight: 600 }}>已选 {selectedTokenIds.length} 项</span>
|
isMobile={isMobile}
|
||||||
|
info={`已选 ${selectedTokenIds.length} 项`}
|
||||||
|
desktopStyle={{ marginBottom: 12 }}
|
||||||
|
>
|
||||||
<button onClick={() => runBatchTokenAction('enable')} disabled={batchActionLoading} className="btn btn-ghost" style={{ border: '1px solid var(--color-border)' }}>
|
<button onClick={() => runBatchTokenAction('enable')} disabled={batchActionLoading} className="btn btn-ghost" style={{ border: '1px solid var(--color-border)' }}>
|
||||||
批量启用
|
批量启用
|
||||||
</button>
|
</button>
|
||||||
@@ -1008,21 +1011,7 @@ export function TokensPanel({ embedded = false, onEmbeddedActionsChange }: Token
|
|||||||
<button data-testid="tokens-batch-delete" onClick={() => runBatchTokenAction('delete')} disabled={batchActionLoading} className="btn btn-link btn-link-danger">
|
<button data-testid="tokens-batch-delete" onClick={() => runBatchTokenAction('delete')} disabled={batchActionLoading} className="btn btn-link btn-link-danger">
|
||||||
批量删除
|
批量删除
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</ResponsiveBatchActionBar>
|
||||||
)}
|
|
||||||
|
|
||||||
{isMobile && selectedTokenIds.length > 0 && (
|
|
||||||
<MobileBatchBar info={`已选 ${selectedTokenIds.length} 项`}>
|
|
||||||
<button onClick={() => runBatchTokenAction('enable')} disabled={batchActionLoading} className="btn btn-ghost" style={{ border: '1px solid var(--color-border)' }}>
|
|
||||||
批量启用
|
|
||||||
</button>
|
|
||||||
<button onClick={() => runBatchTokenAction('disable')} disabled={batchActionLoading} className="btn btn-ghost" style={{ border: '1px solid var(--color-border)' }}>
|
|
||||||
批量禁用
|
|
||||||
</button>
|
|
||||||
<button data-testid="tokens-batch-delete" onClick={() => runBatchTokenAction('delete')} disabled={batchActionLoading} className="btn btn-link btn-link-danger">
|
|
||||||
批量删除
|
|
||||||
</button>
|
|
||||||
</MobileBatchBar>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<CenteredModal
|
<CenteredModal
|
||||||
|
|||||||
Reference in New Issue
Block a user