🐛 fix(ui): 修复新版数据视图布局与 AI 面板加载容错

- 修复新版数据视图底部分页、列快速定位与当前页查找的对齐和压缩问题
- 优化窄屏下 AI 面板布局,避免挤压工作区并增加懒加载失败重试兜底
- 补充窗口运行时、AI 面板布局与 UI 回归测试,更新相关样式快照
This commit is contained in:
Syngnat
2026-05-28 07:05:48 +08:00
parent fac826b335
commit 8131ea8fc8
19 changed files with 1281 additions and 129 deletions

View 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}');
});
});

View File

@@ -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);

View File

@@ -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>

View File

@@ -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);

View File

@@ -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', () => {

View File

@@ -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}

View 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;

View File

@@ -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}

View File

@@ -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}

View File

@@ -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="每页条数"
/>

View File

@@ -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"

View File

@@ -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,

View File

@@ -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,

View 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);
});
});

View 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);
};

View 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);
});
});

View 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;
}
};

View File

@@ -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;
}