mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-31 13:11:45 +08:00
🐛 fix(ui): 修复新版数据视图布局与 AI 面板加载容错
- 修复新版数据视图底部分页、列快速定位与当前页查找的对齐和压缩问题 - 优化窄屏下 AI 面板布局,避免挤压工作区并增加懒加载失败重试兜底 - 补充窗口运行时、AI 面板布局与 UI 回归测试,更新相关样式快照
This commit is contained in:
@@ -1 +1 @@
|
||||
0295a42fd931778d85157816d79d29e5
|
||||
d0464f9da25e9356e61652e638c99ffe
|
||||
20
frontend/src/App.ai-panel-error-boundary.test.ts
Normal file
20
frontend/src/App.ai-panel-error-boundary.test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const appSource = readFileSync(
|
||||
fileURLToPath(new globalThis.URL('./App.tsx', import.meta.url)),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
describe('AI panel lazy-load guard', () => {
|
||||
it('keeps AI panel failures scoped to the panel area with retry support', () => {
|
||||
expect(appSource).toContain('const createLazyAIChatPanel = () => React.lazy(() => import(\'./components/AIChatPanel\'));');
|
||||
expect(appSource).toContain('class AIPanelErrorBoundary extends React.Component');
|
||||
expect(appSource).toContain('<AIPanelErrorBoundary');
|
||||
expect(appSource).toContain('AI 面板加载失败');
|
||||
expect(appSource).toContain('重新加载');
|
||||
expect(appSource).toContain('setAiPanelRenderNonce((current) => current + 1)');
|
||||
expect(appSource).toContain('<LazyAIChatPanel width={aiPanelRenderWidth}');
|
||||
});
|
||||
});
|
||||
@@ -14,7 +14,7 @@ const getGlobalShortcutCaseBlock = (action: string) => {
|
||||
|
||||
const afterCase = appSource.slice(start + caseToken.length);
|
||||
const nextCaseIndex = afterCase.search(/\n\s+case '[^']+':/);
|
||||
const switchEndIndex = afterCase.search(/\n\s+}\n\s+};\n\n\s+window\.addEventListener\('keydown', handleGlobalShortcut\);/);
|
||||
const switchEndIndex = afterCase.indexOf("window.addEventListener('keydown', handleGlobalShortcut);");
|
||||
const endIndex = nextCaseIndex >= 0 ? nextCaseIndex : switchEndIndex;
|
||||
|
||||
expect(endIndex).toBeGreaterThan(-1);
|
||||
|
||||
@@ -89,12 +89,12 @@ import {
|
||||
resolveLegacyAIEdgeHandleDockStyle,
|
||||
resolveLegacyAIEdgeHandleStyle,
|
||||
} from './utils/aiEntryLayout';
|
||||
import { DEFAULT_AI_PANEL_WIDTH, resolveOverlayAIPanelWidth, shouldOverlayAIPanel } from './utils/aiPanelLayout';
|
||||
import { safeWindowRuntimeCall } from './utils/wailsRuntime';
|
||||
import { ApplyDataRootDirectory, GetDataRootDirectoryInfo, GetSavedConnections, OpenDataRootDirectory, SelectDataRootDirectory, SetMacNativeWindowControls, SetWindowTranslucency } from '../wailsjs/go/app/App';
|
||||
import './App.css';
|
||||
import './v2-theme.css';
|
||||
|
||||
const AIChatPanel = React.lazy(() => import('./components/AIChatPanel'));
|
||||
|
||||
const { Sider, Content } = Layout;
|
||||
const SIDEBAR_RESIZE_MIN_WIDTH = 200;
|
||||
const SIDEBAR_RESIZE_MAX_WIDTH = 600;
|
||||
@@ -187,6 +187,45 @@ const createClosedConnectionPackageDialogState = (): ConnectionPackageDialogStat
|
||||
confirmLoading: false,
|
||||
});
|
||||
|
||||
const createLazyAIChatPanel = () => React.lazy(() => import('./components/AIChatPanel'));
|
||||
|
||||
interface AIPanelErrorBoundaryProps {
|
||||
children: React.ReactNode;
|
||||
fallback: (error: Error | null) => React.ReactNode;
|
||||
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
|
||||
}
|
||||
|
||||
interface AIPanelErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
class AIPanelErrorBoundary extends React.Component<
|
||||
AIPanelErrorBoundaryProps,
|
||||
AIPanelErrorBoundaryState
|
||||
> {
|
||||
constructor(props: AIPanelErrorBoundaryProps) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): AIPanelErrorBoundaryState {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
this.props.onError?.(error, errorInfo);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return this.props.fallback(this.state.error);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isSyncModalOpen, setIsSyncModalOpen] = useState(false);
|
||||
@@ -244,6 +283,7 @@ function App() {
|
||||
const [isLinuxRuntime, setIsLinuxRuntime] = useState(false);
|
||||
const [isStoreHydrated, setIsStoreHydrated] = useState(() => useStore.persist.hasHydrated());
|
||||
const [hasLoadedSecureConfig, setHasLoadedSecureConfig] = useState(false);
|
||||
const [viewportWidth, setViewportWidth] = useState(() => (typeof window === 'undefined' ? 1280 : window.innerWidth || 1280));
|
||||
const [securityUpdateStatus, setSecurityUpdateStatus] = useState<SecurityUpdateStatus>(() => createEmptySecurityUpdateStatus());
|
||||
const [securityUpdateRawPayload, setSecurityUpdateRawPayload] = useState<string | null>(null);
|
||||
const [securityUpdateHasLegacySensitiveItems, setSecurityUpdateHasLegacySensitiveItems] = useState(false);
|
||||
@@ -258,6 +298,7 @@ function App() {
|
||||
const [focusedAIProviderId, setFocusedAIProviderId] = useState<string | undefined>(undefined);
|
||||
const [connectionPackageDialog, setConnectionPackageDialog] = useState<ConnectionPackageDialogState>(() => createClosedConnectionPackageDialogState());
|
||||
const [pendingConnectionImportPayload, setPendingConnectionImportPayload] = useState<string | null>(null);
|
||||
const [aiPanelRenderNonce, setAiPanelRenderNonce] = useState(0);
|
||||
const sidebarWidth = useStore(state => state.sidebarWidth);
|
||||
const setSidebarWidth = useStore(state => state.setSidebarWidth);
|
||||
const aiPanelVisible = useStore(state => state.aiPanelVisible);
|
||||
@@ -269,6 +310,7 @@ function App() {
|
||||
const windowDiagLastSignatureRef = React.useRef('');
|
||||
const windowDiagLastAtRef = React.useRef(0);
|
||||
const connectionWorkbenchState = getConnectionWorkbenchState(isStoreHydrated, hasLoadedSecureConfig);
|
||||
const LazyAIChatPanel = useMemo(() => createLazyAIChatPanel(), [aiPanelRenderNonce]);
|
||||
const securityUpdateStatusMeta = useMemo(
|
||||
() => getSecurityUpdateStatusMeta(securityUpdateStatus),
|
||||
[securityUpdateStatus],
|
||||
@@ -279,6 +321,18 @@ function App() {
|
||||
);
|
||||
|
||||
const windowCornerRadius = 14;
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
const syncViewportWidth = () => {
|
||||
setViewportWidth(window.innerWidth || document.documentElement?.clientWidth || 1280);
|
||||
};
|
||||
syncViewportWidth();
|
||||
window.addEventListener('resize', syncViewportWidth);
|
||||
return () => window.removeEventListener('resize', syncViewportWidth);
|
||||
}, []);
|
||||
|
||||
useEffect(()=>{
|
||||
if (typeof document === 'undefined' || !document.body) {
|
||||
return;
|
||||
@@ -629,8 +683,8 @@ function App() {
|
||||
const saveWindowState = async () => {
|
||||
try {
|
||||
const [isFs, isMax] = await Promise.all([
|
||||
WindowIsFullscreen().catch(() => false),
|
||||
WindowIsMaximised().catch(() => false),
|
||||
safeWindowRuntimeCall(() => WindowIsFullscreen(), false),
|
||||
safeWindowRuntimeCall(() => WindowIsMaximised(), false),
|
||||
]);
|
||||
|
||||
// 保存窗口状态
|
||||
@@ -648,8 +702,8 @@ function App() {
|
||||
if (isFs || isMax) return;
|
||||
|
||||
const [size, pos] = await Promise.all([
|
||||
WindowGetSize().catch(() => null),
|
||||
WindowGetPosition().catch(() => null),
|
||||
safeWindowRuntimeCall(() => WindowGetSize(), null),
|
||||
safeWindowRuntimeCall(() => WindowGetPosition(), null),
|
||||
]);
|
||||
if (!size || !pos) return;
|
||||
const w = Math.trunc(Number(size.w || 0));
|
||||
@@ -697,8 +751,8 @@ function App() {
|
||||
inFlight = true;
|
||||
try {
|
||||
const [isFullscreen, isMaximised] = await Promise.all([
|
||||
WindowIsFullscreen().catch(() => false),
|
||||
WindowIsMaximised().catch(() => false),
|
||||
safeWindowRuntimeCall(() => WindowIsFullscreen(), false),
|
||||
safeWindowRuntimeCall(() => WindowIsMaximised(), false),
|
||||
]);
|
||||
|
||||
// 全屏状态下只广播 resize,避免破坏用户的全屏上下文。
|
||||
@@ -708,7 +762,7 @@ function App() {
|
||||
return;
|
||||
}
|
||||
|
||||
const size = await WindowGetSize().catch(() => null);
|
||||
const size = await safeWindowRuntimeCall(() => WindowGetSize(), null);
|
||||
const width = Math.trunc(Number(size?.w || 0));
|
||||
const height = Math.trunc(Number(size?.h || 0));
|
||||
const hasViewportScaleDrift = hasWindowsViewportScaleDrift({
|
||||
@@ -782,7 +836,7 @@ function App() {
|
||||
|
||||
const rememberMinimisedState = async (): Promise<boolean> => {
|
||||
if (cancelled) return false;
|
||||
const isMinimised = await WindowIsMinimised().catch(() => false);
|
||||
const isMinimised = await safeWindowRuntimeCall(() => WindowIsMinimised(), false);
|
||||
if (isMinimised) {
|
||||
minimisedSeen = true;
|
||||
}
|
||||
@@ -1336,12 +1390,12 @@ function App() {
|
||||
}
|
||||
try {
|
||||
const [isFullscreen, isMaximised, isMinimised, isNormal, size, position] = await Promise.all([
|
||||
WindowIsFullscreen().catch(() => false),
|
||||
WindowIsMaximised().catch(() => false),
|
||||
WindowIsMinimised().catch(() => false),
|
||||
WindowIsNormal().catch(() => false),
|
||||
WindowGetSize().catch(() => null),
|
||||
WindowGetPosition().catch(() => null),
|
||||
safeWindowRuntimeCall(() => WindowIsFullscreen(), false),
|
||||
safeWindowRuntimeCall(() => WindowIsMaximised(), false),
|
||||
safeWindowRuntimeCall(() => WindowIsMinimised(), false),
|
||||
safeWindowRuntimeCall(() => WindowIsNormal(), false),
|
||||
safeWindowRuntimeCall(() => WindowGetSize(), null),
|
||||
safeWindowRuntimeCall(() => WindowGetPosition(), null),
|
||||
]);
|
||||
const payload = {
|
||||
seq: ++windowDiagSequenceRef.current,
|
||||
@@ -2024,6 +2078,19 @@ function App() {
|
||||
const [isAISettingsOpen, setIsAISettingsOpen] = useState(false);
|
||||
const aiEntryPlacement = resolveAIEntryPlacement();
|
||||
const legacyAiEdgeHandleAttachment = resolveLegacyAIEdgeHandleAttachment(aiPanelVisible);
|
||||
const aiPanelOverlayActive = aiPanelVisible && shouldOverlayAIPanel({
|
||||
isV2Ui,
|
||||
viewportWidth,
|
||||
sidebarWidth,
|
||||
panelWidth: DEFAULT_AI_PANEL_WIDTH,
|
||||
});
|
||||
const aiPanelRenderWidth = aiPanelOverlayActive
|
||||
? resolveOverlayAIPanelWidth({
|
||||
viewportWidth,
|
||||
sidebarWidth,
|
||||
panelWidth: DEFAULT_AI_PANEL_WIDTH,
|
||||
})
|
||||
: DEFAULT_AI_PANEL_WIDTH;
|
||||
const legacyAiEdgeHandleDockStyle = useMemo(
|
||||
() => resolveLegacyAIEdgeHandleDockStyle(legacyAiEdgeHandleAttachment),
|
||||
[legacyAiEdgeHandleAttachment],
|
||||
@@ -2332,6 +2399,14 @@ function App() {
|
||||
setIsAISettingsOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleAIPanelRenderError = useCallback((error: Error, errorInfo: React.ErrorInfo) => {
|
||||
console.error('AIChatPanel render error:', error, errorInfo);
|
||||
}, []);
|
||||
|
||||
const handleRetryAIPanelRender = useCallback(() => {
|
||||
setAiPanelRenderNonce((current) => current + 1);
|
||||
}, []);
|
||||
|
||||
const handleCloseAISettings = useCallback(() => {
|
||||
const reopenSecurityUpdateDetails = shouldReopenSecurityUpdateDetails(securityUpdateRepairSource);
|
||||
setIsAISettingsOpen(false);
|
||||
@@ -2346,8 +2421,8 @@ function App() {
|
||||
const syncWindowStateFromRuntime = async () => {
|
||||
try {
|
||||
const [isFullscreen, isMaximised] = await Promise.all([
|
||||
WindowIsFullscreen().catch(() => false),
|
||||
WindowIsMaximised().catch(() => false),
|
||||
safeWindowRuntimeCall(() => WindowIsFullscreen(), false),
|
||||
safeWindowRuntimeCall(() => WindowIsMaximised(), false),
|
||||
]);
|
||||
useStore.getState().setWindowState(isFullscreen ? 'fullscreen' : (isMaximised ? 'maximized' : 'normal'));
|
||||
} catch {
|
||||
@@ -2369,7 +2444,7 @@ function App() {
|
||||
void emitWindowDiagnostic('action:titlebar-toggle:after-fullscreen');
|
||||
return;
|
||||
}
|
||||
const isMaximised = await WindowIsMaximised().catch(() => false);
|
||||
const isMaximised = await safeWindowRuntimeCall(() => WindowIsMaximised(), false);
|
||||
if (isMaximised) {
|
||||
WindowUnmaximise();
|
||||
} else {
|
||||
@@ -2413,19 +2488,19 @@ function App() {
|
||||
console.warn('ResetWebViewZoom backend unavailable, falling back to maximise toggle', e);
|
||||
}
|
||||
try {
|
||||
const isFullscreen = await WindowIsFullscreen().catch(() => false);
|
||||
const isFullscreen = await safeWindowRuntimeCall(() => WindowIsFullscreen(), false);
|
||||
if (isFullscreen) {
|
||||
message.info('全屏状态下无法重置缩放,请先退出全屏');
|
||||
return;
|
||||
}
|
||||
const isMaximised = await WindowIsMaximised().catch(() => false);
|
||||
const isMaximised = await safeWindowRuntimeCall(() => WindowIsMaximised(), false);
|
||||
if (isMaximised) {
|
||||
WindowUnmaximise();
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 96));
|
||||
WindowMaximise();
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 96));
|
||||
} else {
|
||||
const size = await WindowGetSize().catch(() => null);
|
||||
const size = await safeWindowRuntimeCall(() => WindowGetSize(), null);
|
||||
const width = Math.trunc(Number(size?.w) || 0);
|
||||
const height = Math.trunc(Number(size?.h) || 0);
|
||||
if (width > 0 && height > 0) {
|
||||
@@ -3157,7 +3232,42 @@ function App() {
|
||||
</>
|
||||
)}
|
||||
{aiPanelVisible && (
|
||||
<div style={{ position: 'relative', display: 'flex', flexShrink: 0, overflow: 'visible' }}>
|
||||
<div
|
||||
className={aiPanelOverlayActive ? 'gn-v2-ai-panel-overlay' : undefined}
|
||||
style={aiPanelOverlayActive
|
||||
? { position: 'absolute', inset: 0, display: 'flex', justifyContent: 'flex-end', pointerEvents: 'none', zIndex: 14 }
|
||||
: { position: 'relative', display: 'flex', flexShrink: 0, overflow: 'visible' }}
|
||||
>
|
||||
{aiPanelOverlayActive && (
|
||||
<button
|
||||
type="button"
|
||||
className="gn-v2-ai-panel-backdrop"
|
||||
aria-label="关闭 AI 面板"
|
||||
onClick={() => setAIPanelVisible(false)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
border: 0,
|
||||
padding: 0,
|
||||
background: darkMode ? 'rgba(3, 7, 18, 0.26)' : 'rgba(248, 250, 252, 0.38)',
|
||||
backdropFilter: 'blur(2px)',
|
||||
pointerEvents: 'auto',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={`gn-v2-ai-panel-dock${aiPanelOverlayActive ? ' is-overlay' : ''}`}
|
||||
style={aiPanelOverlayActive
|
||||
? {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
pointerEvents: 'auto',
|
||||
zIndex: 1,
|
||||
boxShadow: '0 18px 48px rgba(15, 23, 42, 0.18)',
|
||||
}
|
||||
: undefined}
|
||||
>
|
||||
{!isV2Ui && (
|
||||
<>
|
||||
{aiEntryPlacement === 'content-edge' && legacyAiEdgeHandleAttachment === 'panel-shell' && (
|
||||
@@ -3167,11 +3277,69 @@ function App() {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<React.Suspense fallback={<div style={{ width: 360, display: 'flex', alignItems: 'center', justifyContent: 'center' }}><Spin size="small" /></div>}>
|
||||
<AIChatPanel darkMode={darkMode} bgColor={bgContent} onClose={() => setAIPanelVisible(false)} onOpenSettings={() => {
|
||||
handleOpenAISettings();
|
||||
}} overlayTheme={overlayTheme} />
|
||||
</React.Suspense>
|
||||
<AIPanelErrorBoundary
|
||||
onError={handleAIPanelRenderError}
|
||||
fallback={(error) => (
|
||||
<div
|
||||
style={{
|
||||
width: aiPanelRenderWidth,
|
||||
minWidth: 0,
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 20,
|
||||
background: bgContent,
|
||||
color: darkMode ? 'rgba(255,255,255,0.88)' : '#162033',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: 360,
|
||||
display: 'grid',
|
||||
gap: 12,
|
||||
padding: 18,
|
||||
borderRadius: 16,
|
||||
border: darkMode ? '1px solid rgba(255,255,255,0.12)' : '1px solid rgba(15,23,42,0.08)',
|
||||
background: darkMode ? 'rgba(15,23,42,0.72)' : 'rgba(255,255,255,0.94)',
|
||||
boxShadow: darkMode ? '0 16px 36px rgba(0,0,0,0.32)' : '0 16px 36px rgba(15,23,42,0.12)',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 15, fontWeight: 600 }}>AI 面板加载失败</div>
|
||||
<div style={{ fontSize: 12, lineHeight: 1.6, color: darkMode ? 'rgba(255,255,255,0.68)' : '#526075' }}>
|
||||
这通常是开发环境热更新后懒加载资源失效导致的。已阻止整页白屏,你可以直接重试。
|
||||
</div>
|
||||
{error?.message && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
lineHeight: 1.5,
|
||||
wordBreak: 'break-word',
|
||||
padding: '10px 12px',
|
||||
borderRadius: 10,
|
||||
background: darkMode ? 'rgba(2,6,23,0.7)' : 'rgba(248,250,252,0.92)',
|
||||
border: darkMode ? '1px solid rgba(148,163,184,0.18)' : '1px solid rgba(148,163,184,0.22)',
|
||||
}}
|
||||
>
|
||||
{error.message}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: 'flex', gap: 10, justifyContent: 'flex-end' }}>
|
||||
<Button onClick={() => setAIPanelVisible(false)}>关闭面板</Button>
|
||||
<Button type="primary" onClick={handleRetryAIPanelRender}>重新加载</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<React.Suspense fallback={<div style={{ width: aiPanelRenderWidth, display: 'flex', alignItems: 'center', justifyContent: 'center' }}><Spin size="small" /></div>}>
|
||||
<LazyAIChatPanel width={aiPanelRenderWidth} darkMode={darkMode} bgColor={bgContent} onClose={() => setAIPanelVisible(false)} onOpenSettings={() => {
|
||||
handleOpenAISettings();
|
||||
}} overlayTheme={overlayTheme} />
|
||||
</React.Suspense>
|
||||
</AIPanelErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -246,6 +246,11 @@ export const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
||||
const pendingJVMPlanContextRef = useRef<JVMAIPlanContext | undefined>(undefined);
|
||||
const pendingJVMDiagnosticPlanContextRef = useRef<JVMDiagnosticPlanContext | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
setPanelWidth(width);
|
||||
dragWidthRef.current = width;
|
||||
}, [width]);
|
||||
|
||||
const aiChatHistory = useStore(state => state.aiChatHistory);
|
||||
const aiActiveSessionId = useStore(state => state.aiActiveSessionId);
|
||||
const appearance = useStore(state => state.appearance);
|
||||
|
||||
@@ -91,13 +91,14 @@ describe('DataGrid layout', () => {
|
||||
expect(markup).toContain('data-grid-secondary-actions="true"');
|
||||
expect(markup).toContain('data-grid-view-switcher="true"');
|
||||
expect(markup).toContain('data-grid-column-display-action="true"');
|
||||
expect(markup).toContain('data-grid-column-quick-find-action="true"');
|
||||
expect(markup).toContain('字段显示');
|
||||
expect(markup).toContain('跳列');
|
||||
expect(markup).toContain('data-grid-page-find="true"');
|
||||
expect(markup).toContain('data-grid-page-find-prev="true"');
|
||||
expect(markup).toContain('data-grid-page-find-next="true"');
|
||||
expect(markup).not.toContain('gn-v2-data-grid-status-right');
|
||||
expect(markup).not.toContain('gn-v2-data-grid-status-spacer');
|
||||
expect(markup).toContain('gn-v2-data-grid-pagination-spacer');
|
||||
expect(markup).toContain('gn-v2-data-grid-status-main');
|
||||
expect(markup).toContain('gn-v2-data-grid-status-right');
|
||||
expect(markup).toContain('data-grid-v2-pagination="true"');
|
||||
expect(markup).toContain('data-grid-v2-page-chip="true"');
|
||||
expect(markup).toContain('data-grid-v2-pagination-prev="true"');
|
||||
@@ -367,8 +368,19 @@ describe('DataGrid layout', () => {
|
||||
|
||||
it('keeps DataGrid scroll synchronization throttled to animation frames', () => {
|
||||
const source = readFileSync(new URL('./DataGrid.tsx', import.meta.url), 'utf8');
|
||||
const secondaryActionsSource = readFileSync(new URL('./DataGridSecondaryActions.tsx', import.meta.url), 'utf8');
|
||||
const columnTitleSource = readFileSync(new URL('./DataGridColumnTitle.tsx', import.meta.url), 'utf8');
|
||||
const columnQuickFindSource = readFileSync(new URL('./DataGridColumnQuickFind.tsx', import.meta.url), 'utf8');
|
||||
const paginationBarSource = readFileSync(new URL('./DataGridPaginationBar.tsx', import.meta.url), 'utf8');
|
||||
const css = readFileSync(new URL('../v2-theme.css', import.meta.url), 'utf8');
|
||||
|
||||
expect(source).toContain('virtualHorizontalElementsRef');
|
||||
expect(source).toContain('const handleSubmitColumnQuickFind = useCallback(() => {');
|
||||
expect(source).toContain('resolveDataGridColumnQuickFindScrollLeft({');
|
||||
expect(source).toContain('const applied = applyVirtualHorizontalOffset(tableContainer, nextScrollLeft);');
|
||||
expect(source).toContain('syncExternalScrollFromTargets();');
|
||||
expect(source).toContain("const columnQuickFindContent = isTableSurfaceActive ? (");
|
||||
expect(secondaryActionsSource).toContain('data-grid-column-quick-find-action="true"');
|
||||
expect(source).toContain('type VirtualTableScrollReference = TableReference & {');
|
||||
expect(source).toContain('const tableRef = useRef<VirtualTableScrollReference | null>(null);');
|
||||
expect(source).toContain('resolveDataGridHorizontalWheelDelta({');
|
||||
@@ -406,6 +418,17 @@ describe('DataGrid layout', () => {
|
||||
expect(source).toContain('scrollSnapshotRafRef.current = requestAnimationFrame');
|
||||
expect(source).toContain("const dataGridBackdropFilter = isV2Ui || isMacLike ? 'none' : (opacity < 0.999 ? 'blur(14px)' : 'none');");
|
||||
expect(source).toContain('rowHoverable={!enableVirtual}');
|
||||
expect(columnTitleSource).toContain("data-grid-column-highlighted={highlighted ? 'true' : undefined}");
|
||||
expect(columnTitleSource).toContain('data-column-name={normalizedName}');
|
||||
expect(columnQuickFindSource).toContain('AutoComplete');
|
||||
expect(columnQuickFindSource).toContain('placeholder="跳到字段列..."');
|
||||
expect(secondaryActionsSource.indexOf('{pageFindContent}')).toBeLessThan(secondaryActionsSource.indexOf('gn-v2-data-grid-status-center'));
|
||||
expect(css).toContain('width: 66px !important;');
|
||||
expect(css).toContain('grid-template-columns: 160px 26px 26px !important;');
|
||||
expect(css).toContain('.data-grid-pagination-size-select.ant-select-focused .ant-select-selector');
|
||||
expect(css).toContain('overflow-x: auto;');
|
||||
expect(paginationBarSource).toContain("label: `${value}/页`");
|
||||
expect(css).toContain('background: transparent !important;');
|
||||
});
|
||||
|
||||
it('keeps the DataGrid performance harness aligned with legacy and v2 comparison controls', () => {
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
calculateExternalHorizontalScrollInnerWidth,
|
||||
calculateTableBodyBottomPadding,
|
||||
calculateVirtualTableScrollX,
|
||||
resolveDataGridColumnQuickFindScrollLeft,
|
||||
resolveDataGridHorizontalWheelDelta,
|
||||
} from './dataGridLayout';
|
||||
import {
|
||||
@@ -110,6 +111,7 @@ import {
|
||||
} from './V2TableContextMenu';
|
||||
import DataGridColumnTitle from './DataGridColumnTitle';
|
||||
import DataGridColumnInfoPopoverContent from './DataGridColumnInfoPopoverContent';
|
||||
import DataGridColumnQuickFind from './DataGridColumnQuickFind';
|
||||
import DataGridPageFind from './DataGridPageFind';
|
||||
import DataGridPaginationBar from './DataGridPaginationBar';
|
||||
import DataGridResultViewSwitcher from './DataGridResultViewSwitcher';
|
||||
@@ -1559,16 +1561,32 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
const [displayColumnNames, setDisplayColumnNames] = useState<string[]>([]);
|
||||
const [localHiddenColumns, setLocalHiddenColumns] = useState<string[]>([]);
|
||||
const [columnSearchText, setColumnSearchText] = useState('');
|
||||
const [columnQuickFindText, setColumnQuickFindText] = useState('');
|
||||
const [highlightedColumnName, setHighlightedColumnName] = useState('');
|
||||
const [pageFindText, setPageFindText] = useState('');
|
||||
const [activePageFindMatchIndex, setActivePageFindMatchIndex] = useState(-1);
|
||||
const columnQuickFindHighlightTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const deferredPageFindText = useDeferredValue(pageFindText);
|
||||
const deferredColumnQuickFindText = useDeferredValue(columnQuickFindText);
|
||||
const normalizedPageFindText = useMemo(() => normalizeDataGridFindQuery(deferredPageFindText), [deferredPageFindText]);
|
||||
const normalizedColumnQuickFindText = useMemo(
|
||||
() => normalizeDataGridFindQuery(deferredColumnQuickFindText),
|
||||
[deferredColumnQuickFindText],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setColumnQuickFindText('');
|
||||
setHighlightedColumnName('');
|
||||
setPageFindText('');
|
||||
setActivePageFindMatchIndex(-1);
|
||||
}, [connectionId, dbName, tableName]);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (columnQuickFindHighlightTimerRef.current) {
|
||||
clearTimeout(columnQuickFindHighlightTimerRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Sync hidden columns from store
|
||||
useEffect(() => {
|
||||
if (enableHiddenColumnMemory && connectionId && dbName && tableName) {
|
||||
@@ -2349,10 +2367,11 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
columnMetaHintColor={columnMetaHintColor}
|
||||
columnMetaTooltipColor={columnMetaTooltipColor}
|
||||
darkMode={darkMode}
|
||||
highlighted={highlightedColumnName === normalizedName}
|
||||
onOpenForeignKey={foreignKeyTarget ? () => openForeignKeyTarget(foreignKeyTarget) : undefined}
|
||||
/>
|
||||
);
|
||||
}, [columnMetaHintColor, columnMetaTooltipColor, columnMetaMap, columnMetaMapByLowerName, darkMode, densityParams.metaFontSize, foreignKeyMap, foreignKeyMapByLowerName, openForeignKeyTarget, showColumnComment, showColumnType]);
|
||||
}, [columnMetaHintColor, columnMetaTooltipColor, columnMetaMap, columnMetaMapByLowerName, darkMode, densityParams.metaFontSize, foreignKeyMap, foreignKeyMapByLowerName, highlightedColumnName, openForeignKeyTarget, showColumnComment, showColumnType]);
|
||||
|
||||
const lockVirtualInlineTableScroll = useCallback((lock: boolean) => {
|
||||
if (lock) {
|
||||
@@ -2875,13 +2894,18 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
height: 100%;
|
||||
}
|
||||
.${gridId} .data-grid-pagination-size-select {
|
||||
min-width: 112px;
|
||||
width: 72px;
|
||||
min-width: 72px;
|
||||
max-width: 72px;
|
||||
height: 34px;
|
||||
display: inline-flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
.${gridId} .data-grid-pagination-size-select.ant-select-single,
|
||||
.${gridId} .data-grid-pagination-size-select.ant-select-single.ant-select-sm {
|
||||
width: 72px;
|
||||
min-width: 72px;
|
||||
max-width: 72px;
|
||||
height: 34px;
|
||||
}
|
||||
.${gridId} .data-grid-pagination-size-select .ant-select-selector {
|
||||
@@ -2890,7 +2914,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
border: 1px solid ${paginationChipBorderColor} !important;
|
||||
background: ${paginationChipBg} !important;
|
||||
box-shadow: none !important;
|
||||
padding: 0 12px !important;
|
||||
padding: 0 24px 0 10px !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
@@ -2911,15 +2935,16 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
line-height: 34px !important;
|
||||
color: ${paginationPrimaryTextColor};
|
||||
font-weight: 600;
|
||||
justify-content: flex-start;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.${gridId} .data-grid-pagination-size-select .ant-select-selection-search {
|
||||
inset-inline-start: 12px !important;
|
||||
inset-inline-end: 32px !important;
|
||||
inset-inline-start: 10px !important;
|
||||
inset-inline-end: 24px !important;
|
||||
}
|
||||
.${gridId} .data-grid-pagination-size-select .ant-select-arrow {
|
||||
color: ${paginationSecondaryTextColor};
|
||||
inset-inline-end: 12px;
|
||||
inset-inline-end: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
margin-top: 0;
|
||||
@@ -6246,6 +6271,37 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
if (match) focusPageFindMatch(match);
|
||||
}, [activePageFindMatchIndex, pageFindMatches, focusPageFindMatch]);
|
||||
|
||||
const visibleColumnQuickFindMatches = useMemo(() => {
|
||||
if (!normalizedColumnQuickFindText) return [];
|
||||
return displayColumnNames.filter((columnName) => (
|
||||
normalizeDataGridFindQuery(columnName).includes(normalizedColumnQuickFindText)
|
||||
));
|
||||
}, [displayColumnNames, normalizedColumnQuickFindText]);
|
||||
|
||||
const columnQuickFindOptions = useMemo(
|
||||
() => visibleColumnQuickFindMatches.slice(0, 12).map((columnName) => ({ value: columnName, label: columnName })),
|
||||
[visibleColumnQuickFindMatches],
|
||||
);
|
||||
|
||||
const resolveColumnQuickFindTarget = useCallback((): string => {
|
||||
const exactMatch = displayColumnNames.find((columnName) => (
|
||||
normalizeDataGridFindQuery(columnName) === normalizedColumnQuickFindText
|
||||
));
|
||||
if (exactMatch) return exactMatch;
|
||||
return visibleColumnQuickFindMatches[0] || '';
|
||||
}, [displayColumnNames, normalizedColumnQuickFindText, visibleColumnQuickFindMatches]);
|
||||
|
||||
const highlightColumnQuickFindTarget = useCallback((columnName: string) => {
|
||||
setHighlightedColumnName(columnName);
|
||||
if (columnQuickFindHighlightTimerRef.current) {
|
||||
clearTimeout(columnQuickFindHighlightTimerRef.current);
|
||||
}
|
||||
columnQuickFindHighlightTimerRef.current = setTimeout(() => {
|
||||
setHighlightedColumnName((prev) => (prev === columnName ? '' : prev));
|
||||
columnQuickFindHighlightTimerRef.current = null;
|
||||
}, 1600);
|
||||
}, []);
|
||||
|
||||
const syncExternalScrollFromTargets = useCallback((targets?: HTMLElement[], source?: HTMLElement | null) => {
|
||||
const externalScroll = externalHorizontalScrollRef.current;
|
||||
if (!(externalScroll instanceof HTMLDivElement) || horizontalSyncSourceRef.current === 'external') {
|
||||
@@ -6392,6 +6448,107 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
});
|
||||
}, [applyVirtualHorizontalOffset, enableVirtual, readVirtualHorizontalOffset, syncVirtualHorizontalVisualOffset]);
|
||||
|
||||
const focusColumnQuickFindTarget = useCallback((columnName: string): boolean => {
|
||||
const root = rootRef.current;
|
||||
const tableContainer = tableContainerRef.current;
|
||||
if (!(root instanceof HTMLElement) || !(tableContainer instanceof HTMLElement)) return false;
|
||||
const headerTarget = Array.from(root.querySelectorAll('[data-column-name]')).find((node) => {
|
||||
const el = node as HTMLElement;
|
||||
return el.getAttribute('data-column-name') === columnName;
|
||||
}) as HTMLElement | undefined;
|
||||
if (!headerTarget) return false;
|
||||
|
||||
const externalScroll = externalHorizontalScrollRef.current;
|
||||
const tableToExternalTargets = pickTableToExternalSyncTargets(tableContainer);
|
||||
const referenceScrollTarget =
|
||||
tableToExternalTargets.find((target) => target.scrollWidth > target.clientWidth + 1)
|
||||
|| tableToExternalTargets[0]
|
||||
|| (tableContainer.querySelector('.ant-table-header') as HTMLElement | null);
|
||||
if (!(referenceScrollTarget instanceof HTMLElement)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentScrollLeft = enableVirtual
|
||||
? readVirtualHorizontalOffset(tableContainer)
|
||||
: referenceScrollTarget.scrollLeft;
|
||||
const targetRect = headerTarget.getBoundingClientRect();
|
||||
const viewportRect = referenceScrollTarget.getBoundingClientRect();
|
||||
const nextScrollLeft = resolveDataGridColumnQuickFindScrollLeft({
|
||||
currentScrollLeft,
|
||||
columnLeft: currentScrollLeft + (targetRect.left - viewportRect.left),
|
||||
columnWidth: targetRect.width,
|
||||
viewportWidth: referenceScrollTarget.clientWidth,
|
||||
scrollWidth: referenceScrollTarget.scrollWidth,
|
||||
});
|
||||
|
||||
if (enableVirtual) {
|
||||
const applied = applyVirtualHorizontalOffset(tableContainer, nextScrollLeft);
|
||||
if (applied) {
|
||||
lastTableScrollLeftRef.current = readVirtualHorizontalOffset(tableContainer);
|
||||
syncExternalScrollFromTargets();
|
||||
requestAnimationFrame(() => {
|
||||
syncExternalScrollFromTargets();
|
||||
});
|
||||
} else {
|
||||
tableToExternalTargets.forEach((target) => {
|
||||
if (target.scrollWidth <= target.clientWidth + 1) {
|
||||
return;
|
||||
}
|
||||
if (Math.abs(target.scrollLeft - nextScrollLeft) > 1) {
|
||||
target.scrollLeft = nextScrollLeft;
|
||||
}
|
||||
});
|
||||
lastTableScrollLeftRef.current = nextScrollLeft;
|
||||
syncExternalScrollFromTargets(tableToExternalTargets, tableToExternalTargets[0] ?? referenceScrollTarget);
|
||||
}
|
||||
} else {
|
||||
const targets = pickHorizontalScrollTargets(tableContainer);
|
||||
const liveTargets = targets.length > 0 ? targets : tableToExternalTargets;
|
||||
liveTargets.forEach((target) => {
|
||||
if (target.scrollWidth <= target.clientWidth + 1) {
|
||||
return;
|
||||
}
|
||||
if (Math.abs(target.scrollLeft - nextScrollLeft) > 1) {
|
||||
target.scrollLeft = nextScrollLeft;
|
||||
}
|
||||
});
|
||||
lastTableScrollLeftRef.current = nextScrollLeft;
|
||||
scheduleSyncExternalScrollFromTargets(liveTargets[0] ?? referenceScrollTarget);
|
||||
}
|
||||
|
||||
highlightColumnQuickFindTarget(columnName);
|
||||
return true;
|
||||
}, [
|
||||
applyVirtualHorizontalOffset,
|
||||
enableVirtual,
|
||||
highlightColumnQuickFindTarget,
|
||||
pickHorizontalScrollTargets,
|
||||
pickTableToExternalSyncTargets,
|
||||
readVirtualHorizontalOffset,
|
||||
scheduleSyncExternalScrollFromTargets,
|
||||
syncExternalScrollFromTargets,
|
||||
]);
|
||||
|
||||
const handleSubmitColumnQuickFind = useCallback(() => {
|
||||
const targetColumnName = resolveColumnQuickFindTarget();
|
||||
if (!targetColumnName) {
|
||||
if (columnQuickFindText.trim()) {
|
||||
void message.warning(`未找到字段列:${columnQuickFindText.trim()}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
setColumnQuickFindText(targetColumnName);
|
||||
const tryFocus = () => focusColumnQuickFindTarget(targetColumnName);
|
||||
if (tryFocus()) return;
|
||||
requestAnimationFrame(() => {
|
||||
if (tryFocus()) return;
|
||||
requestAnimationFrame(() => {
|
||||
if (tryFocus()) return;
|
||||
void message.warning(`字段列“${targetColumnName}”当前未渲染,无法定位`);
|
||||
});
|
||||
});
|
||||
}, [columnQuickFindText, focusColumnQuickFindTarget, resolveColumnQuickFindTarget]);
|
||||
|
||||
// 外部水平滚动条的 wheel 处理(通过原生事件绑定,确保 preventDefault 生效)
|
||||
useEffect(() => {
|
||||
const externalScroll = externalHorizontalScrollRef.current;
|
||||
@@ -6872,6 +7029,18 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
onNavigateNext={() => handleNavigatePageFind('next')}
|
||||
/>
|
||||
);
|
||||
const columnQuickFindContent = isTableSurfaceActive ? (
|
||||
<DataGridColumnQuickFind
|
||||
isV2Ui={isV2Ui}
|
||||
darkMode={darkMode}
|
||||
inputProps={noAutoCapInputProps as Record<string, unknown>}
|
||||
value={columnQuickFindText}
|
||||
options={columnQuickFindOptions}
|
||||
hasTarget={!!resolveColumnQuickFindTarget()}
|
||||
onChange={setColumnQuickFindText}
|
||||
onSubmit={handleSubmitColumnQuickFind}
|
||||
/>
|
||||
) : null;
|
||||
const resultViewSwitcher = (
|
||||
<DataGridResultViewSwitcher
|
||||
isV2Ui={isV2Ui}
|
||||
@@ -7344,6 +7513,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
pendingChangeCount={pendingChangeCount}
|
||||
resultViewSwitcher={resultViewSwitcher}
|
||||
columnInfoSettingContent={columnInfoSettingContent}
|
||||
columnQuickFindContent={columnQuickFindContent}
|
||||
pageFindContent={pageFindContent}
|
||||
paginationContent={paginationContent}
|
||||
onViewModeChange={handleViewModeChange}
|
||||
|
||||
77
frontend/src/components/DataGridColumnQuickFind.tsx
Normal file
77
frontend/src/components/DataGridColumnQuickFind.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React from 'react';
|
||||
import { AutoComplete, Button, Input, Tooltip } from 'antd';
|
||||
import { AimOutlined, SearchOutlined } from '@ant-design/icons';
|
||||
|
||||
export interface DataGridColumnQuickFindProps {
|
||||
isV2Ui: boolean;
|
||||
darkMode: boolean;
|
||||
inputProps?: Record<string, unknown>;
|
||||
value: string;
|
||||
options: Array<{ value: string; label?: React.ReactNode }>;
|
||||
hasTarget: boolean;
|
||||
onChange: (value: string) => void;
|
||||
onSubmit: () => void;
|
||||
}
|
||||
|
||||
const DataGridColumnQuickFind: React.FC<DataGridColumnQuickFindProps> = ({
|
||||
isV2Ui,
|
||||
darkMode,
|
||||
inputProps,
|
||||
value,
|
||||
options,
|
||||
hasTarget,
|
||||
onChange,
|
||||
onSubmit,
|
||||
}) => (
|
||||
<Tooltip title="输入字段名,回车或点定位按钮即可跳到对应列">
|
||||
<div
|
||||
data-grid-column-quick-find="true"
|
||||
className={isV2Ui ? 'gn-v2-data-grid-column-quick-find' : undefined}
|
||||
style={isV2Ui ? undefined : { display: 'flex', alignItems: 'center', gap: 6 }}
|
||||
>
|
||||
<div className={isV2Ui ? 'gn-v2-data-grid-column-quick-find-row' : undefined}>
|
||||
<div className={isV2Ui ? 'gn-v2-data-grid-column-quick-find-field' : undefined}>
|
||||
<AutoComplete
|
||||
className={isV2Ui ? 'gn-v2-data-grid-column-quick-find-autocomplete' : undefined}
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onSelect={onChange}
|
||||
filterOption={false}
|
||||
popupMatchSelectWidth={280}
|
||||
>
|
||||
<Input
|
||||
{...inputProps}
|
||||
allowClear
|
||||
size="small"
|
||||
variant="borderless"
|
||||
prefix={<SearchOutlined />}
|
||||
placeholder="跳到字段列..."
|
||||
value={value}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
onPressEnter={onSubmit}
|
||||
style={isV2Ui ? undefined : { width: 220 }}
|
||||
/>
|
||||
</AutoComplete>
|
||||
</div>
|
||||
<Button
|
||||
data-grid-column-quick-find-submit="true"
|
||||
className={isV2Ui ? 'gn-v2-data-grid-column-quick-find-submit' : undefined}
|
||||
size="small"
|
||||
icon={<AimOutlined />}
|
||||
disabled={!hasTarget}
|
||||
onClick={onSubmit}
|
||||
>
|
||||
{isV2Ui ? null : '跳转'}
|
||||
</Button>
|
||||
</div>
|
||||
{!isV2Ui && (
|
||||
<span style={{ fontSize: 12, color: darkMode ? '#999' : '#666', whiteSpace: 'nowrap' }}>
|
||||
定位字段列
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
export default DataGridColumnQuickFind;
|
||||
@@ -18,6 +18,7 @@ export interface DataGridColumnTitleProps {
|
||||
columnMetaHintColor: string;
|
||||
columnMetaTooltipColor: string;
|
||||
darkMode: boolean;
|
||||
highlighted?: boolean;
|
||||
onOpenForeignKey?: () => void;
|
||||
}
|
||||
|
||||
@@ -31,6 +32,7 @@ const DataGridColumnTitle: React.FC<DataGridColumnTitleProps> = ({
|
||||
columnMetaHintColor,
|
||||
columnMetaTooltipColor,
|
||||
darkMode,
|
||||
highlighted = false,
|
||||
onOpenForeignKey,
|
||||
}) => {
|
||||
const normalizedName = String(columnName || '');
|
||||
@@ -90,6 +92,8 @@ const DataGridColumnTitle: React.FC<DataGridColumnTitleProps> = ({
|
||||
const titleNode = (
|
||||
<div
|
||||
className={isSingleLineColumnTitle ? 'gn-v2-column-title is-single-line' : 'gn-v2-column-title'}
|
||||
data-grid-column-highlighted={highlighted ? 'true' : undefined}
|
||||
data-column-name={normalizedName}
|
||||
data-grid-column-title-single-line={isSingleLineColumnTitle ? 'true' : undefined}
|
||||
style={{
|
||||
display: 'flex',
|
||||
@@ -99,6 +103,11 @@ const DataGridColumnTitle: React.FC<DataGridColumnTitleProps> = ({
|
||||
minWidth: 0,
|
||||
maxWidth: '100%',
|
||||
lineHeight: 1.2,
|
||||
borderRadius: highlighted ? 8 : undefined,
|
||||
background: highlighted ? (darkMode ? 'rgba(250, 173, 20, 0.18)' : 'rgba(250, 173, 20, 0.16)') : undefined,
|
||||
boxShadow: highlighted ? `inset 0 0 0 1px ${darkMode ? 'rgba(250, 173, 20, 0.5)' : 'rgba(250, 173, 20, 0.55)'}` : undefined,
|
||||
padding: highlighted ? '4px 6px' : undefined,
|
||||
transition: 'background 160ms ease, box-shadow 160ms ease',
|
||||
}}
|
||||
>
|
||||
{fieldLabel}
|
||||
|
||||
@@ -39,34 +39,40 @@ const DataGridPageFind: React.FC<DataGridPageFindProps> = ({
|
||||
className={isV2Ui ? 'gn-v2-data-grid-page-find' : undefined}
|
||||
style={isV2Ui ? undefined : { display: 'flex', alignItems: 'center', gap: 6 }}
|
||||
>
|
||||
<Input
|
||||
{...inputProps}
|
||||
allowClear
|
||||
size="small"
|
||||
prefix={<SearchOutlined />}
|
||||
placeholder="当前页查找..."
|
||||
value={pageFindText}
|
||||
onChange={(event) => onPageFindTextChange(event.target.value)}
|
||||
style={isV2Ui ? undefined : { width: 220 }}
|
||||
/>
|
||||
<Button
|
||||
data-grid-page-find-prev="true"
|
||||
size="small"
|
||||
icon={<LeftOutlined />}
|
||||
disabled={!hasMatches}
|
||||
onClick={onNavigatePrevious}
|
||||
>
|
||||
{isV2Ui ? null : '上一个'}
|
||||
</Button>
|
||||
<Button
|
||||
data-grid-page-find-next="true"
|
||||
size="small"
|
||||
icon={<RightOutlined />}
|
||||
disabled={!hasMatches}
|
||||
onClick={onNavigateNext}
|
||||
>
|
||||
{isV2Ui ? null : '下一个'}
|
||||
</Button>
|
||||
<div className={isV2Ui ? 'gn-v2-data-grid-page-find-row' : undefined}>
|
||||
<Input
|
||||
className={isV2Ui ? 'gn-v2-data-grid-page-find-input' : undefined}
|
||||
{...inputProps}
|
||||
allowClear
|
||||
size="small"
|
||||
variant="borderless"
|
||||
prefix={<SearchOutlined />}
|
||||
placeholder="当前页查找..."
|
||||
value={pageFindText}
|
||||
onChange={(event) => onPageFindTextChange(event.target.value)}
|
||||
style={isV2Ui ? undefined : { width: 220 }}
|
||||
/>
|
||||
<Button
|
||||
data-grid-page-find-prev="true"
|
||||
className={isV2Ui ? 'gn-v2-data-grid-page-find-prev' : undefined}
|
||||
size="small"
|
||||
icon={<LeftOutlined />}
|
||||
disabled={!hasMatches}
|
||||
onClick={onNavigatePrevious}
|
||||
>
|
||||
{isV2Ui ? null : '上一个'}
|
||||
</Button>
|
||||
<Button
|
||||
data-grid-page-find-next="true"
|
||||
className={isV2Ui ? 'gn-v2-data-grid-page-find-next' : undefined}
|
||||
size="small"
|
||||
icon={<RightOutlined />}
|
||||
disabled={!hasMatches}
|
||||
onClick={onNavigateNext}
|
||||
>
|
||||
{isV2Ui ? null : '下一个'}
|
||||
</Button>
|
||||
</div>
|
||||
{normalizedPageFindText && (
|
||||
<span aria-live="polite" style={isV2Ui ? undefined : { fontSize: 12, color: darkMode ? '#999' : '#666', whiteSpace: 'nowrap' }}>
|
||||
{hasMatches ? `${activePageFindPosition} / ${matchCount} · ` : ''}匹配 {occurrenceCount} 处 / {matchedCellCount} 个单元格
|
||||
|
||||
@@ -78,7 +78,7 @@ const DataGridPaginationBar: React.FC<DataGridPaginationBarProps> = ({
|
||||
popupMatchSelectWidth={false}
|
||||
value={String(pagination.pageSize)}
|
||||
onChange={onPageSizeChange}
|
||||
options={paginationPageSizeOptions.map((value) => ({ value, label: `${value} /页` }))}
|
||||
options={paginationPageSizeOptions.map((value) => ({ value, label: `${value}/页` }))}
|
||||
className="data-grid-pagination-size-select"
|
||||
aria-label="每页条数"
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Button, Popover } from 'antd';
|
||||
import {
|
||||
AimOutlined,
|
||||
ConsoleSqlOutlined,
|
||||
EditOutlined,
|
||||
FileTextOutlined,
|
||||
@@ -21,6 +22,7 @@ export interface DataGridSecondaryActionsProps {
|
||||
pendingChangeCount: number;
|
||||
resultViewSwitcher: React.ReactNode;
|
||||
columnInfoSettingContent: React.ReactNode;
|
||||
columnQuickFindContent: React.ReactNode;
|
||||
pageFindContent: React.ReactNode;
|
||||
paginationContent: React.ReactNode;
|
||||
onViewModeChange: (nextMode: GridViewMode) => void;
|
||||
@@ -41,6 +43,7 @@ const DataGridSecondaryActions: React.FC<DataGridSecondaryActionsProps> = ({
|
||||
pendingChangeCount,
|
||||
resultViewSwitcher,
|
||||
columnInfoSettingContent,
|
||||
columnQuickFindContent,
|
||||
pageFindContent,
|
||||
paginationContent,
|
||||
onViewModeChange,
|
||||
@@ -59,48 +62,61 @@ const DataGridSecondaryActions: React.FC<DataGridSecondaryActionsProps> = ({
|
||||
|
||||
return (
|
||||
<div data-grid-secondary-actions="true" className="gn-v2-data-grid-statusbar">
|
||||
<div className="gn-v2-data-grid-view-tabs">
|
||||
{viewTabItems.map((item) => (
|
||||
<div className="gn-v2-data-grid-status-main">
|
||||
<div className="gn-v2-data-grid-view-tabs">
|
||||
{viewTabItems.map((item) => (
|
||||
<Button
|
||||
data-grid-ddl-action={item.key === 'ddl' && canViewDdl ? 'true' : undefined}
|
||||
key={item.key}
|
||||
size="small"
|
||||
type={viewMode === item.key || (item.key === 'table' && (viewMode === 'json' || viewMode === 'text')) ? 'primary' : 'text'}
|
||||
icon={item.icon}
|
||||
disabled={item.disabled}
|
||||
loading={item.key === 'ddl' && ddlLoading}
|
||||
onClick={() => {
|
||||
if (item.key === 'table') {
|
||||
onViewModeChange('table');
|
||||
return;
|
||||
}
|
||||
onViewModeChange(item.key);
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<div className="gn-v2-toolbar-divider" />
|
||||
{resultViewSwitcher}
|
||||
<Popover trigger="click" placement="topRight" content={columnInfoSettingContent}>
|
||||
<Button
|
||||
data-grid-ddl-action={item.key === 'ddl' && canViewDdl ? 'true' : undefined}
|
||||
key={item.key}
|
||||
data-grid-column-display-action="true"
|
||||
size="small"
|
||||
type={viewMode === item.key || (item.key === 'table' && (viewMode === 'json' || viewMode === 'text')) ? 'primary' : 'text'}
|
||||
icon={item.icon}
|
||||
disabled={item.disabled}
|
||||
loading={item.key === 'ddl' && ddlLoading}
|
||||
onClick={() => {
|
||||
if (item.key === 'table') {
|
||||
onViewModeChange('table');
|
||||
return;
|
||||
}
|
||||
onViewModeChange(item.key);
|
||||
}}
|
||||
type={showColumnComment || showColumnType ? 'primary' : 'text'}
|
||||
icon={<FileTextOutlined />}
|
||||
>
|
||||
{item.label}
|
||||
字段显示
|
||||
</Button>
|
||||
))}
|
||||
</Popover>
|
||||
<Popover trigger="click" placement="topRight" content={<div style={{ padding: 4 }}>{columnQuickFindContent}</div>}>
|
||||
<Button
|
||||
data-grid-column-quick-find-action="true"
|
||||
size="small"
|
||||
type="text"
|
||||
icon={<AimOutlined />}
|
||||
>
|
||||
跳列
|
||||
</Button>
|
||||
</Popover>
|
||||
{pageFindContent}
|
||||
<div className="gn-v2-data-grid-status-center">
|
||||
<span className="gn-v2-data-grid-live">live</span>
|
||||
<span>{mergedDisplayCount} 行</span>
|
||||
<span>未提交 {pendingChangeCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="gn-v2-toolbar-divider" />
|
||||
{resultViewSwitcher}
|
||||
<Popover trigger="click" placement="topRight" content={columnInfoSettingContent}>
|
||||
<Button
|
||||
data-grid-column-display-action="true"
|
||||
size="small"
|
||||
type={showColumnComment || showColumnType ? 'primary' : 'text'}
|
||||
icon={<FileTextOutlined />}
|
||||
>
|
||||
字段显示
|
||||
</Button>
|
||||
</Popover>
|
||||
<div className="gn-v2-data-grid-status-center">
|
||||
<span className="gn-v2-data-grid-live">live</span>
|
||||
<span>{mergedDisplayCount} 行</span>
|
||||
<span>未提交 {pendingChangeCount}</span>
|
||||
<div className="gn-v2-data-grid-status-right">
|
||||
{paginationContent}
|
||||
</div>
|
||||
{pageFindContent}
|
||||
<div className="gn-v2-data-grid-pagination-spacer" aria-hidden="true" />
|
||||
{paginationContent}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -130,6 +146,7 @@ const DataGridSecondaryActions: React.FC<DataGridSecondaryActionsProps> = ({
|
||||
<Popover trigger="click" placement="bottomRight" content={columnInfoSettingContent}>
|
||||
<Button data-grid-column-display-action="true" icon={<FileTextOutlined />}>字段信息</Button>
|
||||
</Popover>
|
||||
{columnQuickFindContent}
|
||||
{canViewDdl && (
|
||||
<Button
|
||||
data-grid-ddl-action="true"
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
calculateExternalHorizontalScrollInnerWidth,
|
||||
calculateTableBodyBottomPadding,
|
||||
calculateVirtualTableScrollX,
|
||||
resolveDataGridColumnQuickFindScrollLeft,
|
||||
resolveDataGridHorizontalWheelDelta,
|
||||
} from './dataGridLayout';
|
||||
|
||||
@@ -47,6 +48,50 @@ describe('dataGridLayout helpers', () => {
|
||||
})).toBe(1);
|
||||
});
|
||||
|
||||
it('resolves quick-find target scrollLeft by centering the target column when possible', () => {
|
||||
expect(resolveDataGridColumnQuickFindScrollLeft({
|
||||
currentScrollLeft: 0,
|
||||
columnLeft: 900,
|
||||
columnWidth: 120,
|
||||
viewportWidth: 600,
|
||||
scrollWidth: 2000,
|
||||
})).toBe(660);
|
||||
|
||||
expect(resolveDataGridColumnQuickFindScrollLeft({
|
||||
currentScrollLeft: 0,
|
||||
columnLeft: 40,
|
||||
columnWidth: 120,
|
||||
viewportWidth: 600,
|
||||
scrollWidth: 2000,
|
||||
})).toBe(0);
|
||||
|
||||
expect(resolveDataGridColumnQuickFindScrollLeft({
|
||||
currentScrollLeft: 200,
|
||||
columnLeft: 1750,
|
||||
columnWidth: 140,
|
||||
viewportWidth: 600,
|
||||
scrollWidth: 2000,
|
||||
})).toBe(1400);
|
||||
});
|
||||
|
||||
it('falls back safely when quick-find scroll metrics are degenerate', () => {
|
||||
expect(resolveDataGridColumnQuickFindScrollLeft({
|
||||
currentScrollLeft: 120,
|
||||
columnLeft: 900,
|
||||
columnWidth: 720,
|
||||
viewportWidth: 600,
|
||||
scrollWidth: 2000,
|
||||
})).toBe(900);
|
||||
|
||||
expect(resolveDataGridColumnQuickFindScrollLeft({
|
||||
currentScrollLeft: 120,
|
||||
columnLeft: Number.NaN,
|
||||
columnWidth: Number.NaN,
|
||||
viewportWidth: 0,
|
||||
scrollWidth: 2000,
|
||||
})).toBe(0);
|
||||
});
|
||||
|
||||
it('only treats wheel gestures as horizontal when the horizontal intent is strong enough', () => {
|
||||
expect(resolveDataGridHorizontalWheelDelta({
|
||||
deltaX: 18,
|
||||
|
||||
@@ -21,6 +21,14 @@ export interface ExternalHorizontalScrollInnerWidthOptions {
|
||||
trackInset: number;
|
||||
}
|
||||
|
||||
export interface DataGridColumnQuickFindScrollLeftOptions {
|
||||
currentScrollLeft: number;
|
||||
columnLeft: number;
|
||||
columnWidth: number;
|
||||
viewportWidth: number;
|
||||
scrollWidth: number;
|
||||
}
|
||||
|
||||
const MIN_SCROLLBAR_CLEARANCE = 8;
|
||||
const FLOATING_SCROLLBAR_VISUAL_EXTRA = 4;
|
||||
const HORIZONTAL_WHEEL_MIN_DELTA = 0.5;
|
||||
@@ -70,6 +78,35 @@ export const calculateExternalHorizontalScrollInnerWidth = ({
|
||||
return Math.max(1, safeTableScrollWidth - safeTrackInset * 2);
|
||||
};
|
||||
|
||||
export const resolveDataGridColumnQuickFindScrollLeft = ({
|
||||
currentScrollLeft,
|
||||
columnLeft,
|
||||
columnWidth,
|
||||
viewportWidth,
|
||||
scrollWidth,
|
||||
}: DataGridColumnQuickFindScrollLeftOptions): number => {
|
||||
const safeViewportWidth = Math.max(0, Math.floor(viewportWidth));
|
||||
const safeScrollWidth = Math.max(0, Math.ceil(scrollWidth));
|
||||
const maxScrollLeft = Math.max(0, safeScrollWidth - safeViewportWidth);
|
||||
|
||||
if (safeViewportWidth <= 0 || maxScrollLeft <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const safeCurrentScrollLeft = Number.isFinite(currentScrollLeft)
|
||||
? Math.max(0, Math.min(maxScrollLeft, currentScrollLeft))
|
||||
: 0;
|
||||
const safeColumnLeft = Number.isFinite(columnLeft) ? columnLeft : safeCurrentScrollLeft;
|
||||
const safeColumnWidth = Math.max(0, Number.isFinite(columnWidth) ? columnWidth : 0);
|
||||
|
||||
if (safeColumnWidth >= safeViewportWidth) {
|
||||
return Math.max(0, Math.min(maxScrollLeft, safeColumnLeft));
|
||||
}
|
||||
|
||||
const centeredScrollLeft = safeColumnLeft - (safeViewportWidth - safeColumnWidth) / 2;
|
||||
return Math.max(0, Math.min(maxScrollLeft, centeredScrollLeft));
|
||||
};
|
||||
|
||||
export const resolveDataGridHorizontalWheelDelta = ({
|
||||
deltaX,
|
||||
deltaY,
|
||||
|
||||
63
frontend/src/utils/aiPanelLayout.test.ts
Normal file
63
frontend/src/utils/aiPanelLayout.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
DEFAULT_AI_PANEL_WIDTH,
|
||||
resolveOverlayAIPanelWidth,
|
||||
shouldOverlayAIPanel,
|
||||
} from './aiPanelLayout';
|
||||
|
||||
describe('aiPanelLayout', () => {
|
||||
it('keeps the v2 AI panel docked while enough workbench width remains', () => {
|
||||
expect(shouldOverlayAIPanel({
|
||||
isV2Ui: true,
|
||||
viewportWidth: 1440,
|
||||
sidebarWidth: 330,
|
||||
panelWidth: DEFAULT_AI_PANEL_WIDTH,
|
||||
minWorkbenchWidth: 320,
|
||||
})).toBe(false);
|
||||
});
|
||||
|
||||
it('switches the v2 AI panel to overlay mode when docking would crush the workbench', () => {
|
||||
expect(shouldOverlayAIPanel({
|
||||
isV2Ui: true,
|
||||
viewportWidth: 825,
|
||||
sidebarWidth: 330,
|
||||
panelWidth: DEFAULT_AI_PANEL_WIDTH,
|
||||
minWorkbenchWidth: 320,
|
||||
})).toBe(true);
|
||||
});
|
||||
|
||||
it('also protects the legacy UI from being crushed by the AI panel', () => {
|
||||
expect(shouldOverlayAIPanel({
|
||||
isV2Ui: false,
|
||||
viewportWidth: 825,
|
||||
sidebarWidth: 330,
|
||||
})).toBe(true);
|
||||
});
|
||||
|
||||
it('clamps overlay width to the available workspace instead of overflowing', () => {
|
||||
expect(resolveOverlayAIPanelWidth({
|
||||
viewportWidth: 825,
|
||||
sidebarWidth: 330,
|
||||
panelWidth: DEFAULT_AI_PANEL_WIDTH,
|
||||
minOverlayWidth: 260,
|
||||
overlayGap: 12,
|
||||
})).toBe(380);
|
||||
|
||||
expect(resolveOverlayAIPanelWidth({
|
||||
viewportWidth: 620,
|
||||
sidebarWidth: 330,
|
||||
panelWidth: DEFAULT_AI_PANEL_WIDTH,
|
||||
minOverlayWidth: 260,
|
||||
overlayGap: 12,
|
||||
})).toBe(278);
|
||||
|
||||
expect(resolveOverlayAIPanelWidth({
|
||||
viewportWidth: 540,
|
||||
sidebarWidth: 330,
|
||||
panelWidth: DEFAULT_AI_PANEL_WIDTH,
|
||||
minOverlayWidth: 260,
|
||||
overlayGap: 12,
|
||||
})).toBe(210);
|
||||
});
|
||||
});
|
||||
61
frontend/src/utils/aiPanelLayout.ts
Normal file
61
frontend/src/utils/aiPanelLayout.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
export const DEFAULT_AI_PANEL_WIDTH = 380;
|
||||
export const MIN_WORKBENCH_WIDTH_WHEN_AI_DOCKED = 320;
|
||||
export const MIN_AI_PANEL_OVERLAY_WIDTH = 260;
|
||||
export const AI_PANEL_OVERLAY_GAP = 12;
|
||||
|
||||
interface AIPanelLayoutOptions {
|
||||
isV2Ui: boolean;
|
||||
viewportWidth: number;
|
||||
sidebarWidth: number;
|
||||
panelWidth?: number;
|
||||
minWorkbenchWidth?: number;
|
||||
}
|
||||
|
||||
interface AIPanelOverlayWidthOptions {
|
||||
viewportWidth: number;
|
||||
sidebarWidth: number;
|
||||
panelWidth?: number;
|
||||
minOverlayWidth?: number;
|
||||
overlayGap?: number;
|
||||
}
|
||||
|
||||
const normalizePositiveNumber = (value: number, fallback: number) => {
|
||||
const normalized = Number(value);
|
||||
return Number.isFinite(normalized) && normalized > 0 ? normalized : fallback;
|
||||
};
|
||||
|
||||
export const shouldOverlayAIPanel = ({
|
||||
isV2Ui,
|
||||
viewportWidth,
|
||||
sidebarWidth,
|
||||
panelWidth = DEFAULT_AI_PANEL_WIDTH,
|
||||
minWorkbenchWidth = MIN_WORKBENCH_WIDTH_WHEN_AI_DOCKED,
|
||||
}: AIPanelLayoutOptions): boolean => {
|
||||
const safeViewportWidth = normalizePositiveNumber(viewportWidth, 0);
|
||||
const safeSidebarWidth = Math.max(0, normalizePositiveNumber(sidebarWidth, 0));
|
||||
const safePanelWidth = Math.max(0, normalizePositiveNumber(panelWidth, DEFAULT_AI_PANEL_WIDTH));
|
||||
const safeMinWorkbenchWidth = Math.max(0, normalizePositiveNumber(minWorkbenchWidth, MIN_WORKBENCH_WIDTH_WHEN_AI_DOCKED));
|
||||
const workspaceWidth = Math.max(0, safeViewportWidth - safeSidebarWidth);
|
||||
|
||||
void isV2Ui;
|
||||
return workspaceWidth - safePanelWidth < safeMinWorkbenchWidth;
|
||||
};
|
||||
|
||||
export const resolveOverlayAIPanelWidth = ({
|
||||
viewportWidth,
|
||||
sidebarWidth,
|
||||
panelWidth = DEFAULT_AI_PANEL_WIDTH,
|
||||
minOverlayWidth = MIN_AI_PANEL_OVERLAY_WIDTH,
|
||||
overlayGap = AI_PANEL_OVERLAY_GAP,
|
||||
}: AIPanelOverlayWidthOptions): number => {
|
||||
const safeViewportWidth = normalizePositiveNumber(viewportWidth, panelWidth);
|
||||
const safeSidebarWidth = Math.max(0, normalizePositiveNumber(sidebarWidth, 0));
|
||||
const safePanelWidth = Math.max(0, normalizePositiveNumber(panelWidth, DEFAULT_AI_PANEL_WIDTH));
|
||||
const safeMinOverlayWidth = Math.max(0, normalizePositiveNumber(minOverlayWidth, MIN_AI_PANEL_OVERLAY_WIDTH));
|
||||
const safeOverlayGap = Math.max(0, normalizePositiveNumber(overlayGap, AI_PANEL_OVERLAY_GAP));
|
||||
const workspaceWidth = Math.max(0, safeViewportWidth - safeSidebarWidth);
|
||||
const preferredWidth = Math.min(safePanelWidth, Math.max(0, workspaceWidth - safeOverlayGap));
|
||||
const lowerBound = Math.min(safeMinOverlayWidth, workspaceWidth);
|
||||
|
||||
return Math.max(lowerBound, preferredWidth);
|
||||
};
|
||||
21
frontend/src/utils/wailsRuntime.test.ts
Normal file
21
frontend/src/utils/wailsRuntime.test.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { safeWindowRuntimeCall } from './wailsRuntime';
|
||||
|
||||
describe('safeWindowRuntimeCall', () => {
|
||||
it('accepts synchronous runtime return values', async () => {
|
||||
await expect(safeWindowRuntimeCall(() => true, false)).resolves.toBe(true);
|
||||
await expect(safeWindowRuntimeCall(() => ({ w: 1280, h: 720 }), null)).resolves.toEqual({ w: 1280, h: 720 });
|
||||
});
|
||||
|
||||
it('keeps supporting Promise based runtime return values', async () => {
|
||||
await expect(safeWindowRuntimeCall(async () => false, true)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('falls back when the runtime call throws or rejects', async () => {
|
||||
await expect(safeWindowRuntimeCall(() => {
|
||||
throw new Error('sync failure');
|
||||
}, false)).resolves.toBe(false);
|
||||
await expect(safeWindowRuntimeCall(async () => Promise.reject(new Error('async failure')), true)).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
10
frontend/src/utils/wailsRuntime.ts
Normal file
10
frontend/src/utils/wailsRuntime.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export const safeWindowRuntimeCall = async <T>(
|
||||
invoke: () => T | Promise<T>,
|
||||
fallback: T,
|
||||
): Promise<T> => {
|
||||
try {
|
||||
return await Promise.resolve().then(invoke);
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
@@ -2584,6 +2584,37 @@ body[data-ui-version="v2"] .gn-v2-ai-panel-dock {
|
||||
background: var(--gn-bg-panel);
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-ai-panel-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
pointer-events: none;
|
||||
z-index: 14;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-ai-panel-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
background: rgba(248, 250, 252, 0.38);
|
||||
backdrop-filter: blur(2px);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-ai-panel-dock.is-overlay {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
pointer-events: auto;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-ai-panel-dock.is-overlay .gn-v2-ai-panel {
|
||||
height: 100%;
|
||||
box-shadow: 0 18px 48px rgba(15, 23, 42, 0.18);
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-tab-workbench {
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
@@ -3105,11 +3136,40 @@ body[data-ui-version="v2"] .gn-v2-data-grid-statusbar {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-status-main {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: calc(8px * var(--gn-ui-scale, 1));
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-status-main::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-status-right {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex: 0 0 auto;
|
||||
min-width: fit-content;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-statusbar {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-status-main {
|
||||
flex: 0 0 auto;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-status-center {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
@@ -3145,7 +3205,8 @@ body[data-ui-version="v2"] .gn-v2-data-grid-result-switcher,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-status-center,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-pagination-wrap,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid .data-grid-pagination-shell,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-page-find {
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-page-find,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
align-self: center;
|
||||
@@ -3245,14 +3306,15 @@ body[data-ui-version="v2"] .gn-v2-data-grid-result-switcher .ant-segmented-item-
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-status-center {
|
||||
height: var(--gn-v2-statusbar-control-height);
|
||||
min-height: var(--gn-v2-statusbar-control-height);
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
flex: 0 0 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
min-width: fit-content;
|
||||
overflow: visible;
|
||||
color: var(--gn-fg-4);
|
||||
font-family: var(--gn-font-mono);
|
||||
font-size: 10.5px;
|
||||
font-size: 10px;
|
||||
line-height: 26px;
|
||||
opacity: 0.88;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-status-center > span {
|
||||
@@ -3261,6 +3323,7 @@ body[data-ui-version="v2"] .gn-v2-data-grid-status-center > span {
|
||||
height: var(--gn-v2-statusbar-control-height);
|
||||
flex: 0 0 auto;
|
||||
line-height: 26px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-status-center > span:not(:last-child)::after {
|
||||
@@ -3293,20 +3356,184 @@ body[data-ui-version="v2"] .gn-v2-data-grid-live::before {
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-page-find {
|
||||
height: var(--gn-v2-statusbar-control-height);
|
||||
min-height: var(--gn-v2-statusbar-control-height);
|
||||
gap: 2px;
|
||||
flex: 0 0 auto;
|
||||
max-width: 214px;
|
||||
margin-left: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find {
|
||||
display: grid !important;
|
||||
grid-template-columns: 180px 28px;
|
||||
column-gap: 4px;
|
||||
height: 28px;
|
||||
min-height: 28px;
|
||||
gap: 4px;
|
||||
flex: 0 0 auto;
|
||||
max-width: 244px;
|
||||
margin-left: 0;
|
||||
overflow: hidden;
|
||||
align-items: stretch;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 0 180px;
|
||||
width: 180px;
|
||||
min-width: 180px;
|
||||
height: 28px;
|
||||
min-height: 28px;
|
||||
align-self: stretch;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .ant-select {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
flex: 0 0 180px;
|
||||
height: 28px !important;
|
||||
min-height: 28px !important;
|
||||
width: 180px !important;
|
||||
min-width: 180px;
|
||||
align-self: stretch;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .ant-input-affix-wrapper,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .ant-select-selector {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
width: 180px !important;
|
||||
min-width: 180px;
|
||||
height: 28px !important;
|
||||
min-height: 28px !important;
|
||||
line-height: 28px !important;
|
||||
border-radius: 6px !important;
|
||||
border: 0.5px solid var(--gn-br-2) !important;
|
||||
background: var(--gn-bg-input) !important;
|
||||
box-shadow: none !important;
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .ant-btn {
|
||||
align-self: stretch;
|
||||
width: 28px !important;
|
||||
min-width: 28px !important;
|
||||
height: 28px !important;
|
||||
min-height: 28px !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
line-height: 1 !important;
|
||||
border-width: 0.5px !important;
|
||||
border-style: solid !important;
|
||||
border-color: var(--gn-br-2) !important;
|
||||
border-radius: 6px !important;
|
||||
background: var(--gn-bg-input) !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
box-sizing: border-box !important;
|
||||
vertical-align: top !important;
|
||||
position: relative;
|
||||
top: 0 !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find-submit {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
flex: 0 0 28px;
|
||||
width: 28px !important;
|
||||
min-width: 28px !important;
|
||||
height: 28px !important;
|
||||
min-height: 28px !important;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-page-find .ant-input-affix-wrapper {
|
||||
width: 156px !important;
|
||||
min-width: 120px;
|
||||
width: 160px !important;
|
||||
min-width: 160px;
|
||||
max-width: 160px !important;
|
||||
height: 26px !important;
|
||||
min-height: 26px !important;
|
||||
line-height: 26px !important;
|
||||
border-radius: 6px !important;
|
||||
border: 0.5px solid var(--gn-br-2) !important;
|
||||
background: var(--gn-bg-input) !important;
|
||||
box-shadow: none !important;
|
||||
padding-inline: 8px !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-page-find .ant-input-affix-wrapper .ant-input,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .ant-input-affix-wrapper .ant-input {
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-page-find .ant-input-affix-wrapper > input.ant-input:focus,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-page-find .ant-input-affix-wrapper > input.ant-input:focus-visible,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .ant-input-affix-wrapper > input.ant-input:focus,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .ant-input-affix-wrapper > input.ant-input:focus-visible {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-page-find .ant-input-affix-wrapper.ant-input-affix-wrapper-focused,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .ant-input-affix-wrapper.ant-input-affix-wrapper-focused {
|
||||
border-color: color-mix(in srgb, var(--gn-info) 38%, var(--gn-br-2)) !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .ant-btn:focus,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .ant-btn:focus-visible,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .ant-btn:active,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .ant-btn.ant-btn-default:focus,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .ant-btn.ant-btn-default:focus-visible,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .ant-btn.ant-btn-icon-only:focus,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .ant-btn.ant-btn-icon-only:focus-visible {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
border-color: var(--gn-br-3) !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .ant-btn::after,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .ant-btn:focus::after,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .ant-btn:focus-visible::after,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .ant-btn:active::after {
|
||||
display: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .ant-select-selector,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .ant-select-focused .ant-select-selector {
|
||||
border-color: transparent !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
@media (max-width: 1460px) {
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-status-center > span:last-child {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1360px) {
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-status-center > span:nth-child(2) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-page-find .ant-btn {
|
||||
@@ -3318,6 +3545,12 @@ body[data-ui-version="v2"] .gn-v2-data-grid-page-find .ant-btn {
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
line-height: 1 !important;
|
||||
border-width: 0.5px !important;
|
||||
border-style: solid !important;
|
||||
border-color: var(--gn-br-2) !important;
|
||||
border-radius: 6px !important;
|
||||
background: var(--gn-bg-input) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-page-find > span {
|
||||
@@ -3328,7 +3561,13 @@ body[data-ui-version="v2"] .gn-v2-data-grid-page-find > span {
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-statusbar [data-grid-page-find="true"] {
|
||||
padding: 0;
|
||||
border: 0.5px solid transparent;
|
||||
border: none;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-statusbar [data-grid-column-quick-find="true"] {
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
@@ -3336,13 +3575,6 @@ body[data-ui-version="v2"] .gn-v2-data-grid-statusbar [data-grid-view-switcher="
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-pagination-spacer {
|
||||
flex: 1 1 auto;
|
||||
min-width: 12px;
|
||||
height: 1px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-pagination-wrap {
|
||||
height: var(--gn-v2-statusbar-control-height);
|
||||
min-height: var(--gn-v2-statusbar-control-height);
|
||||
@@ -3402,8 +3634,14 @@ body[data-ui-version="v2"] .gn-v2-data-grid-statusbar .anticon svg {
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-pagination-wrap .data-grid-pagination-summary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
margin-right: 2px;
|
||||
gap: 0;
|
||||
margin-right: 0;
|
||||
padding: 0 !important;
|
||||
min-height: 26px !important;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
color: var(--gn-fg-3);
|
||||
font-family: var(--gn-font-mono);
|
||||
font-size: 11.5px;
|
||||
@@ -3413,7 +3651,7 @@ body[data-ui-version="v2"] .gn-v2-data-grid-pagination-wrap .data-grid-paginatio
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 26px;
|
||||
margin-right: 6px;
|
||||
margin-right: 2px;
|
||||
color: var(--gn-fg-1);
|
||||
font-family: var(--gn-font-mono);
|
||||
font-size: 11.5px;
|
||||
@@ -3430,12 +3668,31 @@ body[data-ui-version="v2"] .gn-v2-data-grid-pagination-wrap .ant-btn {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center;
|
||||
border-width: 0.5px !important;
|
||||
border-style: solid !important;
|
||||
border-color: var(--gn-br-2) !important;
|
||||
background: var(--gn-bg-panel) !important;
|
||||
border-radius: 6px !important;
|
||||
background: var(--gn-bg-input) !important;
|
||||
box-shadow: none !important;
|
||||
line-height: 1 !important;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-page-find .ant-btn:hover,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .ant-btn:hover,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-pagination-wrap .ant-btn:hover {
|
||||
border-color: var(--gn-br-3) !important;
|
||||
background: var(--gn-bg-panel-2) !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-page-find .ant-btn:disabled,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .ant-btn:disabled,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-pagination-wrap .ant-btn:disabled {
|
||||
border-color: var(--gn-br-1) !important;
|
||||
background: var(--gn-bg-panel) !important;
|
||||
color: var(--gn-fg-5) !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-pagination-wrap .ant-btn .ant-btn-icon,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-pagination-wrap .ant-btn .anticon,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-pagination-wrap .ant-btn svg {
|
||||
@@ -3455,10 +3712,10 @@ body[data-ui-version="v2"] .gn-v2-data-grid-pagination-wrap .data-grid-paginatio
|
||||
gap: 4px;
|
||||
height: 26px;
|
||||
min-height: 26px;
|
||||
padding: 0 7px;
|
||||
border: 0.5px solid var(--gn-br-2);
|
||||
border-radius: 5px;
|
||||
background: var(--gn-bg-panel);
|
||||
padding: 0 6px;
|
||||
border: 0.5px solid var(--gn-br-1);
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--gn-fg-4);
|
||||
font-family: var(--gn-font-mono);
|
||||
font-size: 11.5px;
|
||||
@@ -3475,6 +3732,9 @@ body[data-ui-version="v2"] .gn-v2-data-grid-pagination-wrap .data-grid-paginatio
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-pagination-wrap .data-grid-pagination-size-select.ant-select-single.ant-select-sm {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
width: 66px !important;
|
||||
min-width: 66px !important;
|
||||
max-width: 66px !important;
|
||||
height: 26px !important;
|
||||
min-height: 26px !important;
|
||||
line-height: 26px !important;
|
||||
@@ -3484,7 +3744,7 @@ body[data-ui-version="v2"] .gn-v2-data-grid-pagination-wrap .data-grid-paginatio
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-pagination-wrap .data-grid-pagination-size-select .ant-select-selector {
|
||||
height: 26px !important;
|
||||
min-height: 26px !important;
|
||||
padding: 0 24px 0 8px !important;
|
||||
padding: 0 20px 0 8px !important;
|
||||
border-radius: 5px !important;
|
||||
font-family: var(--gn-font-mono);
|
||||
font-size: 11px !important;
|
||||
@@ -3497,13 +3757,14 @@ body[data-ui-version="v2"] .gn-v2-data-grid-pagination-wrap .data-grid-paginatio
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-pagination-wrap .data-grid-pagination-size-select .ant-select-selection-item {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: flex-start !important;
|
||||
height: 26px !important;
|
||||
line-height: 26px !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-pagination-wrap .data-grid-pagination-size-select .ant-select-arrow {
|
||||
top: 50% !important;
|
||||
inset-inline-end: 8px !important;
|
||||
inset-inline-end: 6px !important;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-top: 0 !important;
|
||||
@@ -3524,6 +3785,13 @@ body[data-ui-version="v2"] .gn-v2-data-grid-pagination-wrap .data-grid-paginatio
|
||||
line-height: 1 !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-pagination-wrap .data-grid-pagination-size-select.ant-select-focused .ant-select-selector,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-pagination-wrap .data-grid-pagination-size-select.ant-select-open .ant-select-selector {
|
||||
border-color: color-mix(in srgb, var(--gn-info) 38%, var(--gn-br-2)) !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
/* ─── V2 context menus, including portal-rendered custom menus ─ */
|
||||
body[data-ui-version="v2"] .gn-v2-context-menu {
|
||||
width: 260px !important;
|
||||
@@ -5055,3 +5323,155 @@ body[data-ui-version="v2"] .gn-v2-ai-panel .ai-chat-stop-btn {
|
||||
background-color: rgba(220,38,38,0.12) !important;
|
||||
color: var(--gn-danger) !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
height: 28px !important;
|
||||
min-height: 28px !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .gn-v2-data-grid-column-quick-find-row {
|
||||
display: grid !important;
|
||||
grid-template-columns: 180px 28px !important;
|
||||
align-items: stretch !important;
|
||||
column-gap: 4px !important;
|
||||
height: 28px !important;
|
||||
min-height: 28px !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .gn-v2-data-grid-column-quick-find-field,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .gn-v2-data-grid-column-quick-find-autocomplete,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .gn-v2-data-grid-column-quick-find-autocomplete .ant-select,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .gn-v2-data-grid-column-quick-find-field .ant-input-affix-wrapper {
|
||||
width: 180px !important;
|
||||
min-width: 180px !important;
|
||||
height: 28px !important;
|
||||
min-height: 28px !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .gn-v2-data-grid-column-quick-find-field,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .gn-v2-data-grid-column-quick-find-autocomplete,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .gn-v2-data-grid-column-quick-find-autocomplete .ant-select {
|
||||
display: flex !important;
|
||||
align-items: stretch !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .gn-v2-data-grid-column-quick-find-field .ant-input-affix-wrapper {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
line-height: 28px !important;
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .gn-v2-data-grid-column-quick-find-submit.ant-btn {
|
||||
width: 28px !important;
|
||||
min-width: 28px !important;
|
||||
max-width: 28px !important;
|
||||
height: 28px !important;
|
||||
min-height: 28px !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
align-self: stretch !important;
|
||||
justify-self: stretch !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
line-height: 1 !important;
|
||||
vertical-align: top !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .gn-v2-data-grid-column-quick-find-submit.ant-btn > span,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .gn-v2-data-grid-column-quick-find-submit.ant-btn .ant-btn-icon,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .gn-v2-data-grid-column-quick-find-submit.ant-btn .anticon {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
line-height: 1 !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .gn-v2-data-grid-column-quick-find-submit.ant-btn .anticon svg {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .gn-v2-data-grid-column-quick-find-submit.ant-btn:focus,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .gn-v2-data-grid-column-quick-find-submit.ant-btn:focus-visible,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .gn-v2-data-grid-column-quick-find-submit.ant-btn:active {
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .gn-v2-data-grid-column-quick-find-submit.ant-btn::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-page-find {
|
||||
gap: 2px !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-page-find .ant-input-prefix,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-page-find .ant-input-suffix {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-page-find .ant-input-prefix {
|
||||
margin-inline-end: 6px !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-page-find .ant-input,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-page-find .ant-input::placeholder {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-page-find {
|
||||
max-width: 214px !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-page-find .gn-v2-data-grid-page-find-row {
|
||||
display: grid !important;
|
||||
grid-template-columns: 160px 26px 26px !important;
|
||||
column-gap: 2px !important;
|
||||
align-items: stretch !important;
|
||||
height: 26px !important;
|
||||
min-height: 26px !important;
|
||||
flex: 0 0 auto !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-page-find .gn-v2-data-grid-page-find-input,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-page-find .gn-v2-data-grid-page-find-input.ant-input-affix-wrapper {
|
||||
width: 160px !important;
|
||||
min-width: 160px !important;
|
||||
max-width: 160px !important;
|
||||
height: 26px !important;
|
||||
min-height: 26px !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-page-find .gn-v2-data-grid-page-find-input.ant-input-affix-wrapper {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
padding-inline: 8px !important;
|
||||
line-height: 26px !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-page-find .gn-v2-data-grid-page-find-prev.ant-btn,
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-page-find .gn-v2-data-grid-page-find-next.ant-btn {
|
||||
width: 26px !important;
|
||||
min-width: 26px !important;
|
||||
max-width: 26px !important;
|
||||
height: 26px !important;
|
||||
min-height: 26px !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
align-self: stretch !important;
|
||||
justify-self: stretch !important;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user