feat(web-shell): add markdown table column controls

Support resizing, reordering, and freezing table columns while preserving visible-order copy behavior and selection stability.
This commit is contained in:
jifeng.zjd 2026-07-07 17:06:42 +08:00
parent 9a63c03224
commit 6de3f4c19c
4 changed files with 509 additions and 36 deletions

View file

@ -81,6 +81,8 @@
}
.table {
--action-column-width: 72px;
width: max-content;
min-width: 100%;
border-collapse: separate;
@ -112,6 +114,11 @@
text-align: left;
}
.frozenHeaderCell {
left: var(--action-column-width);
z-index: 5;
}
.headerControls {
display: flex;
align-items: stretch;
@ -162,6 +169,43 @@
cursor: pointer;
}
.reorderHandle {
flex: 0 0 28px;
border: none;
border-left: 1px solid var(--border);
background: transparent;
color: var(--muted-foreground);
font: inherit;
cursor: grab;
}
.reorderHandle:active {
cursor: grabbing;
}
.reorderHandle:hover {
background: var(--subtle-bg-strong);
}
.resizeHandle {
position: absolute;
top: 0;
right: -3px;
z-index: 7;
width: 6px;
height: 100%;
padding: 0;
border: none;
background: transparent;
cursor: col-resize;
}
.resizeHandle:hover,
.resizeHandle:focus-visible {
background: color-mix(in srgb, var(--agent-blue-500) 45%, transparent);
outline: none;
}
.filterTriggerActive {
background: color-mix(in srgb, var(--agent-blue-500) 18%, transparent);
color: var(--agent-blue-500);
@ -342,18 +386,35 @@ tr:hover .selectedCell {
.actionHeaderCell,
.actionCell {
width: 72px;
min-width: 72px;
max-width: 72px;
width: var(--action-column-width);
min-width: var(--action-column-width);
max-width: var(--action-column-width);
text-align: center;
}
.stickyActionHeaderCell,
.stickyActionCell {
position: sticky;
left: 0;
z-index: 6;
}
.stickyActionHeaderCell {
z-index: 8;
}
.actionCell {
padding: 4px 6px;
cursor: default;
user-select: none;
}
.frozenCell {
position: sticky;
left: var(--action-column-width);
z-index: 4;
}
.rowDetailButton {
padding: 3px 7px;
border: 1px solid var(--border);

View file

@ -255,6 +255,71 @@ function dispatchCopy(target: Element) {
return { event, setData };
}
function dragColumn(
container: HTMLElement,
fromLabel: string,
toLabel: string,
): void {
const data = new Map<string, string>();
const dataTransfer = {
dropEffect: '',
effectAllowed: '',
get types() {
return [...data.keys()];
},
setData: vi.fn((type: string, value: string) => data.set(type, value)),
getData: vi.fn((type: string) => data.get(type) ?? ''),
};
const from = button(container, fromLabel);
const to = button(container, toLabel);
act(() => {
from.dispatchEvent(
Object.assign(new Event('dragstart', { bubbles: true }), {
dataTransfer,
}),
);
to.dispatchEvent(
Object.assign(
new Event('dragover', { bubbles: true, cancelable: true }),
{
dataTransfer,
},
),
);
to.dispatchEvent(
Object.assign(new Event('drop', { bubbles: true, cancelable: true }), {
dataTransfer,
}),
);
});
}
function dropExternalColumn(container: HTMLElement, toLabel: string): void {
const dataTransfer = {
dropEffect: '',
effectAllowed: '',
types: ['text/plain'],
setData: vi.fn(),
getData: vi.fn(() => ''),
};
const to = button(container, toLabel);
act(() => {
to.dispatchEvent(
Object.assign(
new Event('dragover', { bubbles: true, cancelable: true }),
{
dataTransfer,
},
),
);
to.dispatchEvent(
Object.assign(new Event('drop', { bubbles: true, cancelable: true }), {
dataTransfer,
}),
);
});
}
function touchEvent(
type: string,
touches: Array<Pick<Touch, 'clientX' | 'clientY'>>,
@ -666,6 +731,132 @@ describe('EnhancedMarkdownTable', () => {
);
});
it('resizes a column from its header handle', () => {
const container = renderTable();
const resize = button(container, 'Resize Team');
act(() => {
resize.dispatchEvent(
new MouseEvent('mousedown', {
bubbles: true,
button: 0,
clientX: 100,
}),
);
});
act(() => {
window.dispatchEvent(
new MouseEvent('mousemove', { bubbles: true, clientX: 160 }),
);
window.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
});
expect(button(container, 'Sort by Team').closest('th')?.style.width).toBe(
'220px',
);
expect(dataCell(container, 0, 0).style.width).toBe('220px');
});
it('reorders columns and quick copies in the visible order', () => {
const writeText = mockClipboard();
const container = renderWideTable();
dragColumn(container, 'Move Score', 'Move Team');
expect(rowTexts(container)).toEqual(['10|Alpha|US', '2|Beta|EMEA']);
click(textButton(container, 'Quick copy'));
expect(writeText).toHaveBeenCalledWith(
['Score\tTeam\tRegion', '10\tAlpha\tUS', '2\tBeta\tEMEA'].join('\n'),
);
});
it('ignores external drops on column move handles', () => {
const container = renderWideTable();
dropExternalColumn(container, 'Move Score');
expect(rowTexts(container)).toEqual(['Alpha|US|10', 'Beta|EMEA|2']);
});
it('keeps hidden columns out of reordered selections', () => {
const writeText = mockClipboard();
const container = renderWideTable();
dragColumn(container, 'Move Score', 'Move Team');
click(button(container, 'Filter Region'));
click(textButton(container, 'Hide column'));
dragCells(dataCell(container, 0, 0), dataCell(container, 1, 1));
click(textButton(container, 'Copy TSV'));
expect(writeText).toHaveBeenCalledWith(['10\tAlpha', '2\tBeta'].join('\n'));
});
it('toggles sticky classes for the action column and first visible column', () => {
const container = renderWideTable();
click(textButton(container, 'Freeze first column'));
expect(textButton(container, 'Unfreeze first column')).toBeDefined();
expect(container.querySelector('thead th')?.className).toContain(
'stickyActionHeaderCell',
);
expect(
button(container, 'Sort by Team').closest('th')?.className,
).toContain('frozenHeaderCell');
expect(dataCell(container, 0, 0).className).toContain('frozenCell');
dragColumn(container, 'Move Score', 'Move Team');
expect(
button(container, 'Sort by Score').closest('th')?.className,
).toContain('frozenHeaderCell');
click(textButton(container, 'Unfreeze first column'));
expect(container.textContent).toContain('Freeze first column');
expect(
button(container, 'Sort by Score').closest('th')?.className,
).not.toContain('frozenHeaderCell');
});
it('keeps selected cell classes visible on a frozen column', () => {
const container = renderWideTable();
click(textButton(container, 'Freeze first column'));
dragCells(dataCell(container, 0, 0), dataCell(container, 1, 1));
expect(container.textContent).toContain('4 cells selected');
expect(dataCell(container, 0, 0).className).toContain('frozenCell');
expect(dataCell(container, 0, 0).className).toContain('selectedCell');
expect(dataCell(container, 0, 1).className).toContain('selectedCell');
});
it('copies reordered selections from the keyboard copy event', () => {
const container = renderWideTable();
dragColumn(container, 'Move Score', 'Move Team');
dragCells(dataCell(container, 0, 0), dataCell(container, 1, 1));
const scroller = container.querySelector<HTMLElement>('div[tabindex="0"]');
expect(scroller).not.toBeNull();
const { event, setData } = dispatchCopy(scroller!);
expect(event.defaultPrevented).toBe(true);
expect(setData).toHaveBeenCalledWith(
'text/plain',
['10\tAlpha', '2\tBeta'].join('\n'),
);
});
it('copies reordered selections after freezing the first column', () => {
const writeText = mockClipboard();
const container = renderWideTable();
dragColumn(container, 'Move Score', 'Move Team');
click(textButton(container, 'Freeze first column'));
dragCells(dataCell(container, 0, 0), dataCell(container, 1, 1));
click(textButton(container, 'Copy TSV'));
expect(writeText).toHaveBeenCalledWith(['10\tAlpha', '2\tBeta'].join('\n'));
});
it('shows checkmark feedback after quick copy', async () => {
vi.useFakeTimers();
mockClipboard();
@ -1381,6 +1572,7 @@ describe('EnhancedMarkdownTable', () => {
const container = renderTable('zh-CN');
expect(container.textContent).toContain('快捷复制');
expect(container.textContent).toContain('冻结首列');
expect(container.textContent).toContain('详情');
click(button(container, '筛选 Team'));
expect(container.textContent).toContain('隐藏列');

View file

@ -10,6 +10,7 @@ import {
useState,
type CSSProperties,
type ClipboardEvent,
type DragEvent as ReactDragEvent,
type MouseEvent as ReactMouseEvent,
type ReactElement,
type ReactNode,
@ -81,6 +82,12 @@ interface OpenFilterMenu {
top: number;
}
interface ColumnResizeState {
columnIndex: number;
startX: number;
startWidth: number;
}
interface FilterOption {
value: string;
label: string;
@ -105,6 +112,10 @@ const NUMBER_FILTER_LABEL_KEYS: Record<NumberFilterOperator, string> = {
export const MAX_ENHANCED_TABLE_ROWS = 500;
export const MAX_ENHANCED_TABLE_COLUMNS = 50;
const DEFAULT_COLUMN_WIDTH = 160;
const MIN_COLUMN_WIDTH = 80;
const MAX_COLUMN_WIDTH = 640;
const COLUMN_DRAG_MIME = 'application/x-qwen-web-shell-table-column';
const FOCUSABLE_FILTER_MENU_SELECTOR =
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
@ -282,11 +293,22 @@ function getSelectionBounds(range: SelectionRange) {
return {
minRow: Math.min(range.anchorRow, range.focusRow),
maxRow: Math.max(range.anchorRow, range.focusRow),
minCol: Math.min(range.anchorCol, range.focusCol),
maxCol: Math.max(range.anchorCol, range.focusCol),
};
}
function getSelectedColumnIndexes(
range: SelectionRange | null,
visibleColumnIndexes: number[],
): number[] {
if (!range) return [];
const anchorIndex = visibleColumnIndexes.indexOf(range.anchorCol);
const focusIndex = visibleColumnIndexes.indexOf(range.focusCol);
if (anchorIndex === -1 || focusIndex === -1) return [];
const minIndex = Math.min(anchorIndex, focusIndex);
const maxIndex = Math.max(anchorIndex, focusIndex);
return visibleColumnIndexes.slice(minIndex, maxIndex + 1);
}
function sanitizeForClipboard(value: string): string {
const inspectedValue = value.replace(
/[\u200B-\u200D\u2060\u00AD\uFEFF]/g,
@ -301,10 +323,8 @@ function getSelectionText(
visibleColumnIndexes: number[],
): string {
if (!range) return '';
const { minRow, maxRow, minCol, maxCol } = getSelectionBounds(range);
const selectedColumns = visibleColumnIndexes.filter(
(columnIndex) => columnIndex >= minCol && columnIndex <= maxCol,
);
const { minRow, maxRow } = getSelectionBounds(range);
const selectedColumns = getSelectedColumnIndexes(range, visibleColumnIndexes);
if (selectedColumns.length === 0) return '';
const lines: string[] = [];
@ -350,13 +370,41 @@ function selectionSize(
visibleColumnIndexes: number[],
): number {
if (!range) return 0;
const { minRow, maxRow, minCol, maxCol } = getSelectionBounds(range);
const selectedColumnCount = visibleColumnIndexes.filter(
(columnIndex) => columnIndex >= minCol && columnIndex <= maxCol,
const { minRow, maxRow } = getSelectionBounds(range);
const selectedColumnCount = getSelectedColumnIndexes(
range,
visibleColumnIndexes,
).length;
return (maxRow - minRow + 1) * selectedColumnCount;
}
function moveColumn(order: number[], fromColumn: number, toColumn: number) {
if (fromColumn === toColumn) return order;
const fromIndex = order.indexOf(fromColumn);
const toIndex = order.indexOf(toColumn);
if (fromIndex === -1 || toIndex === -1) return order;
const next = [...order];
const [moved] = next.splice(fromIndex, 1);
if (moved === undefined) return order;
next.splice(toIndex, 0, moved);
return next;
}
function initialColumnOrder(columnCount: number): number[] {
return Array.from({ length: columnCount }, (_, index) => index);
}
function hasColumnDragData(dataTransfer: DataTransfer): boolean {
return Array.from(dataTransfer.types).includes(COLUMN_DRAG_MIME);
}
function getDraggedColumnIndex(dataTransfer: DataTransfer): number | null {
const rawColumnIndex = dataTransfer.getData(COLUMN_DRAG_MIME);
if (rawColumnIndex === '') return null;
const columnIndex = Number(rawColumnIndex);
return Number.isInteger(columnIndex) ? columnIndex : null;
}
function isFilterActive(filter: ColumnFilter | undefined): boolean {
if (!filter) return false;
if (filter.selectedValues !== undefined) return true;
@ -1071,6 +1119,14 @@ export function EnhancedTable({
const [hiddenColumns, setHiddenColumns] = useState<Set<number>>(
() => new Set(),
);
const [columnWidths, setColumnWidths] = useState<Record<number, number>>({});
const [columnOrder, setColumnOrder] = useState<number[]>(() =>
initialColumnOrder(table.columnCount),
);
const [freezeFirstColumn, setFreezeFirstColumn] = useState(false);
const [resizingColumn, setResizingColumn] =
useState<ColumnResizeState | null>(null);
const [draggingColumn, setDraggingColumn] = useState<number | null>(null);
const [detailRowKey, setDetailRowKey] = useState<string | null>(null);
const [isDragging, setIsDragging] = useState(false);
const [copiedVisible, setCopiedVisible] = useState(false);
@ -1189,6 +1245,11 @@ export function EnhancedTable({
setSelection(null);
setOpenFilterMenu(null);
setHiddenColumns(new Set());
setColumnWidths({});
setColumnOrder(initialColumnOrder(table.columnCount));
setFreezeFirstColumn(false);
setResizingColumn(null);
setDraggingColumn(null);
setDetailRowKey(null);
resetCopiedVisible();
resetCopiedSelection();
@ -1303,17 +1364,44 @@ export function EnhancedTable({
),
[table.headers, table.rows],
);
const visibleColumnIndexes = useMemo(
const orderedVisibleColumnIndexes = useMemo(
() =>
table.headers
.map((_, index) => index)
columnOrder
.filter((index) => index >= 0 && index < table.columnCount)
.filter((index) => !hiddenColumns.has(index)),
[hiddenColumns, table.headers],
[columnOrder, hiddenColumns, table.columnCount],
);
const frozenColumnIndex = freezeFirstColumn
? orderedVisibleColumnIndexes[0]
: undefined;
useEffect(() => {
resetCopiedVisible();
}, [resetCopiedVisible, visibleColumnIndexes, visibleRows]);
}, [resetCopiedVisible, orderedVisibleColumnIndexes, visibleRows]);
useEffect(() => {
if (!resizingColumn) return;
const resizeColumn = (event: MouseEvent) => {
const nextWidth = Math.min(
MAX_COLUMN_WIDTH,
Math.max(
MIN_COLUMN_WIDTH,
resizingColumn.startWidth + event.clientX - resizingColumn.startX,
),
);
setColumnWidths((current) => ({
...current,
[resizingColumn.columnIndex]: nextWidth,
}));
};
const stopResize = () => setResizingColumn(null);
window.addEventListener('mousemove', resizeColumn);
window.addEventListener('mouseup', stopResize);
return () => {
window.removeEventListener('mousemove', resizeColumn);
window.removeEventListener('mouseup', stopResize);
};
}, [resizingColumn]);
useEffect(() => {
if (detailRowKey && !visibleRows.some((row) => row.key === detailRowKey)) {
@ -1389,7 +1477,7 @@ export function EnhancedTable({
};
const hideColumn = (columnIndex: number) => {
if (visibleColumnIndexes.length <= 1) return;
if (orderedVisibleColumnIndexes.length <= 1) return;
setSelection(null);
closeFilterMenu();
setFilters((current) => {
@ -1412,6 +1500,10 @@ export function EnhancedTable({
setHiddenColumns(new Set());
};
const toggleFreezeFirstColumn = () => {
setFreezeFirstColumn((current) => !current);
};
const toggleRowDetail = (rowKey: string) => {
setSelection(null);
setDetailRowKey((current) => (current === rowKey ? null : rowKey));
@ -1424,12 +1516,75 @@ export function EnhancedTable({
const isCellSelected = (rowIndex: number, columnIndex: number): boolean => {
if (!selectionBounds) return false;
const { minRow, maxRow, minCol, maxCol } = selectionBounds;
const { minRow, maxRow } = selectionBounds;
const selectedColumnIndexes = getSelectedColumnIndexes(
selection,
orderedVisibleColumnIndexes,
);
return (
rowIndex >= minRow &&
rowIndex <= maxRow &&
columnIndex >= minCol &&
columnIndex <= maxCol
selectedColumnIndexes.includes(columnIndex)
);
};
const columnStyle = (
columnIndex: number,
extra?: CSSProperties,
): CSSProperties => {
const width = columnWidths[columnIndex] ?? DEFAULT_COLUMN_WIDTH;
return {
width,
minWidth: width,
maxWidth: width,
...extra,
};
};
const startColumnResize = (
event: ReactMouseEvent<HTMLButtonElement>,
columnIndex: number,
) => {
event.preventDefault();
event.stopPropagation();
setResizingColumn({
columnIndex,
startX: event.clientX,
startWidth: columnWidths[columnIndex] ?? DEFAULT_COLUMN_WIDTH,
});
};
const startColumnDrag = (
event: ReactDragEvent<HTMLButtonElement>,
columnIndex: number,
) => {
event.stopPropagation();
setDraggingColumn(columnIndex);
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData(COLUMN_DRAG_MIME, String(columnIndex));
};
const dragOverColumn = (event: ReactDragEvent<HTMLButtonElement>) => {
if (draggingColumn === null && !hasColumnDragData(event.dataTransfer)) {
return;
}
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
};
const dropColumn = (
event: ReactDragEvent<HTMLButtonElement>,
targetColumnIndex: number,
) => {
const sourceColumnIndex =
draggingColumn ?? getDraggedColumnIndex(event.dataTransfer);
setDraggingColumn(null);
if (sourceColumnIndex === null) return;
event.preventDefault();
event.stopPropagation();
setSelection(null);
setColumnOrder((current) =>
moveColumn(current, sourceColumnIndex, targetColumnIndex),
);
};
@ -1523,7 +1678,11 @@ export function EnhancedTable({
};
const copySelection = () => {
const text = getSelectionText(selection, visibleRows, visibleColumnIndexes);
const text = getSelectionText(
selection,
visibleRows,
orderedVisibleColumnIndexes,
);
if (!text || !navigator.clipboard) return;
const copyGeneration = copiedSelectionGenRef.current;
void navigator.clipboard
@ -1549,7 +1708,7 @@ export function EnhancedTable({
const text = getVisibleTableText(
table.headers,
visibleRows,
visibleColumnIndexes,
orderedVisibleColumnIndexes,
);
if (!text || !navigator.clipboard) return;
const copyGeneration = copiedVisibleGenRef.current;
@ -1574,13 +1733,17 @@ export function EnhancedTable({
const handleCopy = (event: ClipboardEvent<HTMLDivElement>) => {
if (hasNativeSelection()) return;
const text = getSelectionText(selection, visibleRows, visibleColumnIndexes);
const text = getSelectionText(
selection,
visibleRows,
orderedVisibleColumnIndexes,
);
if (!text) return;
event.preventDefault();
event.clipboardData.setData('text/plain', text);
};
const selectedCount = selectionSize(selection, visibleColumnIndexes);
const selectedCount = selectionSize(selection, orderedVisibleColumnIndexes);
const activeFilterCount =
Object.values(filters).filter(isFilterActive).length;
const rowSummary =
@ -1633,6 +1796,17 @@ export function EnhancedTable({
})}
</button>
)}
{orderedVisibleColumnIndexes.length > 0 && (
<button
className={styles.copyButton}
type="button"
onClick={toggleFreezeFirstColumn}
>
{freezeFirstColumn
? t('markdownTable.unfreezeFirstColumn')
: t('markdownTable.freezeFirstColumn')}
</button>
)}
{activeFilterCount > 0 && (
<span className={styles.selection}>
{t('markdownTable.filtersActive', { count: activeFilterCount })}
@ -1670,10 +1844,14 @@ export function EnhancedTable({
<table className={styles.table}>
<thead>
<tr>
<th className={`${styles.headerCell} ${styles.actionHeaderCell}`}>
<th
className={`${styles.headerCell} ${styles.actionHeaderCell} ${
freezeFirstColumn ? styles.stickyActionHeaderCell : ''
}`}
>
{t('markdownTable.actions')}
</th>
{visibleColumnIndexes.map((columnIndex) => {
{orderedVisibleColumnIndexes.map((columnIndex) => {
const header = table.headers[columnIndex];
if (!header) return null;
const isSorted = sort?.columnIndex === columnIndex;
@ -1704,12 +1882,15 @@ export function EnhancedTable({
const headerAlignStyle = header.textAlign
? { textAlign: header.textAlign }
: undefined;
const isFrozenColumn = frozenColumnIndex === columnIndex;
return (
<th
key={header.key}
className={styles.headerCell}
className={`${styles.headerCell} ${
isFrozenColumn ? styles.frozenHeaderCell : ''
}`}
aria-sort={ariaSort}
style={headerAlignStyle}
style={columnStyle(columnIndex, headerAlignStyle)}
>
<div className={styles.headerControls}>
<button
@ -1729,6 +1910,22 @@ export function EnhancedTable({
{sortLabel}
</span>
</button>
<button
className={styles.reorderHandle}
type="button"
draggable
onDragStart={(event) =>
startColumnDrag(event, columnIndex)
}
onDragOver={dragOverColumn}
onDrop={(event) => dropColumn(event, columnIndex)}
onDragEnd={() => setDraggingColumn(null)}
aria-label={t('markdownTable.moveColumn', {
column: columnName,
})}
>
</button>
<button
className={`${styles.filterTrigger} ${
isFiltered ? styles.filterTriggerActive : ''
@ -1746,6 +1943,16 @@ export function EnhancedTable({
</button>
</div>
<button
className={styles.resizeHandle}
type="button"
onMouseDown={(event) =>
startColumnResize(event, columnIndex)
}
aria-label={t('markdownTable.resizeColumn', {
column: columnName,
})}
/>
</th>
);
})}
@ -1760,7 +1967,11 @@ export function EnhancedTable({
<tr
className={rowIndex % 2 === 1 ? styles.evenRow : undefined}
>
<td className={`${styles.cell} ${styles.actionCell}`}>
<td
className={`${styles.cell} ${styles.actionCell} ${
freezeFirstColumn ? styles.stickyActionCell : ''
}`}
>
<button
className={styles.rowDetailButton}
type="button"
@ -1777,12 +1988,13 @@ export function EnhancedTable({
{t('markdownTable.rowDetails')}
</button>
</td>
{visibleColumnIndexes.map((columnIndex) => {
{orderedVisibleColumnIndexes.map((columnIndex) => {
const cell = row.cells[columnIndex];
if (!cell) return null;
const cellAlignStyle = cell.textAlign
? { textAlign: cell.textAlign }
: undefined;
const isFrozenColumn = frozenColumnIndex === columnIndex;
return (
<td
key={cell.key}
@ -1790,8 +2002,8 @@ export function EnhancedTable({
isCellSelected(rowIndex, columnIndex)
? styles.selectedCell
: ''
}`}
style={cellAlignStyle}
} ${isFrozenColumn ? styles.frozenCell : ''}`}
style={columnStyle(columnIndex, cellAlignStyle)}
data-row-index={rowIndex}
data-column-index={columnIndex}
onMouseDown={(event) =>
@ -1816,13 +2028,13 @@ export function EnhancedTable({
<tr id={detailId} className={styles.detailRow}>
<td
className={styles.detailCell}
colSpan={visibleColumnIndexes.length + 1}
colSpan={orderedVisibleColumnIndexes.length + 1}
>
<div className={styles.detailPanel}>
<div className={styles.detailTitle}>
{t('markdownTable.detailsHeader')}
</div>
{visibleColumnIndexes.map((columnIndex) => {
{orderedVisibleColumnIndexes.map((columnIndex) => {
const header = table.headers[columnIndex];
const cell = row.cells[columnIndex];
if (!header || !cell) return null;
@ -1881,7 +2093,7 @@ export function EnhancedTable({
sort={sort}
style={{ left: openFilterMenu.left, top: openFilterMenu.top }}
menuRef={filterMenuRef}
canHideColumn={visibleColumnIndexes.length > 1}
canHideColumn={orderedVisibleColumnIndexes.length > 1}
onApply={setColumnFilter}
onClose={closeFilterMenu}
onHideColumn={hideColumn}

View file

@ -265,11 +265,15 @@ const EN: Messages = {
},
'markdownTable.copyTsv': 'Copy TSV',
'markdownTable.copyVisible': 'Quick copy',
'markdownTable.freezeFirstColumn': 'Freeze first column',
'markdownTable.unfreezeFirstColumn': 'Unfreeze first column',
'markdownTable.hideColumn': 'Hide column',
'markdownTable.showHiddenColumns': (v) => {
const count = Number(v?.count ?? 0);
return `Show ${count} hidden column${count === 1 ? '' : 's'}`;
},
'markdownTable.moveColumn': (v) => `Move ${v?.column ?? ''}`,
'markdownTable.resizeColumn': (v) => `Resize ${v?.column ?? ''}`,
'markdownTable.rowDetails': 'Details',
'markdownTable.rowDetailsAria': (v) =>
`View details for row ${v?.index ?? ''}`,
@ -1793,8 +1797,12 @@ const ZH: Messages = {
'markdownTable.cellsSelected': (v) => `${v?.count ?? 0} 个单元格已选中`,
'markdownTable.copyTsv': '复制 TSV',
'markdownTable.copyVisible': '快捷复制',
'markdownTable.freezeFirstColumn': '冻结首列',
'markdownTable.unfreezeFirstColumn': '取消冻结首列',
'markdownTable.hideColumn': '隐藏列',
'markdownTable.showHiddenColumns': (v) => `显示 ${v?.count ?? 0} 个隐藏列`,
'markdownTable.moveColumn': (v) => `移动 ${v?.column ?? ''}`,
'markdownTable.resizeColumn': (v) => `调整 ${v?.column ?? ''} 列宽`,
'markdownTable.rowDetails': '详情',
'markdownTable.rowDetailsAria': (v) => `查看第 ${v?.index ?? ''} 行详情`,
'markdownTable.closeRowDetailsAria': (v) => `收起第 ${v?.index ?? ''} 行详情`,