fix(ui): stabilize download table column interactions

This commit is contained in:
NimBold
2026-07-22 10:33:41 +03:30
parent 20a136cd1f
commit 16377aa0d6
5 changed files with 197 additions and 68 deletions
+13 -8
View File
@@ -11,6 +11,7 @@ import {
} from '../utils/downloadProgress';
import {
COLUMN_ALIGNMENT_JUSTIFY,
getColumnGridColumn,
type DownloadColumnAlignment,
type DownloadTableColumnKey
} from '../utils/downloadTableColumns';
@@ -23,7 +24,7 @@ interface DownloadItemProps {
columnOrder: DownloadTableColumnKey[];
columnAlignments: Record<DownloadTableColumnKey, DownloadColumnAlignment>;
tableGridTemplate: string;
tableMinWidth: number;
tableMinWidth: number | string;
setContextMenu: (menu: { x: number; y: number; id: string }) => void;
handlePause: (id: string, skipConfirm?: boolean) => void;
handleResume: (item: DownloadItemType) => void;
@@ -94,12 +95,16 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
const columnStyle = (key: DownloadTableColumnKey): React.CSSProperties => ({
'--column-justify': COLUMN_ALIGNMENT_JUSTIFY[columnAlignments[key]],
gridColumn: getColumnGridColumn(key, columnOrder),
} as React.CSSProperties);
const trailingColumnClass = (key: DownloadTableColumnKey) =>
key === columnOrder[columnOrder.length - 1] ? 'download-column-cell--trailing' : '';
const cells: Record<DownloadTableColumnKey, React.ReactNode> = {
'File Name': (
<div
className="download-column-cell download-file-cell download-column-file-name"
className={`download-column-cell download-file-cell download-column-file-name ${trailingColumnClass('File Name')}`}
style={columnStyle('File Name')}
>
<div className="download-cell-content">
@@ -114,7 +119,7 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
),
Size: (
<div
className="download-column-cell download-cell-truncate download-size-cell tabular-nums"
className={`download-column-cell download-cell-truncate download-size-cell tabular-nums ${trailingColumnClass('Size')}`}
style={columnStyle('Size')}
title={hasDownloadedAmount ? downloadedSizeLabel : completedSizeLabel}
aria-label={hasDownloadedAmount ? downloadedSizeLabel : completedSizeLabel}
@@ -136,7 +141,7 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
),
Status: (
<div
className="download-column-cell download-status-cell"
className={`download-column-cell download-status-cell ${trailingColumnClass('Status')}`}
style={columnStyle('Status')}
>
{download.status === 'completed' ? (
@@ -197,23 +202,23 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
</div>
),
Speed: (
<div className="download-column-cell download-cell-truncate" style={columnStyle('Speed')}>
<div className={`download-column-cell download-cell-truncate ${trailingColumnClass('Speed')}`} style={columnStyle('Speed')}>
<span className="download-cell-content tabular-nums" title={displaySpeed}>
{displaySpeed}
</span>
</div>
),
ETA: (
<div className="download-column-cell download-cell-truncate" style={columnStyle('ETA')}>
<div className={`download-column-cell download-cell-truncate ${trailingColumnClass('ETA')}`} style={columnStyle('ETA')}>
<span className="download-cell-content tabular-nums" title={displayEta}>
{displayEta}
</span>
</div>
),
'Date Added': (
<div className="download-column-cell download-cell-right download-column-date-added" style={columnStyle('Date Added')}>
<div className={`download-column-cell download-cell-right download-column-date-added ${trailingColumnClass('Date Added')}`} style={columnStyle('Date Added')}>
<span
className="download-cell-content download-date-value group-hover:hidden tabular-nums"
className="download-cell-content download-date-value tabular-nums"
title={download.dateAdded ? new Date(download.dateAdded).toLocaleDateString() : '-'}
>
{download.dateAdded ? new Date(download.dateAdded).toLocaleDateString() : '-'}
+128 -37
View File
@@ -37,7 +37,9 @@ import {
DEFAULT_COLUMN_ALIGNMENTS,
DEFAULT_COLUMN_ORDER,
DEFAULT_COLUMN_WIDTHS,
buildColumnGridTemplate,
columnIndex,
getColumnGridColumn,
normalizeColumnAlignments,
normalizeColumnOrder,
normalizeColumnWidths,
@@ -108,6 +110,10 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; id: string } | null>(null);
const [animationParent] = useAutoAnimate<HTMLDivElement>();
const [headerAnimationParent] = useAutoAnimate<HTMLDivElement>({
duration: 140,
easing: 'ease-out',
});
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [lastSelectedId, setLastSelectedId] = useState<string | null>(null);
const [sortConfig, setSortConfig] = useState<DownloadSortConfig>({ column: 'Date Added', direction: 'desc' });
@@ -121,7 +127,9 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
active: boolean;
} | null>(null);
const headerElementsRef = useRef(new Map<DownloadTableColumnKey, HTMLDivElement>());
const headerContainerRef = useRef<HTMLDivElement>(null);
const columnDragStateRef = useRef<ColumnDragState | null>(null);
const columnDragOrderRef = useRef<DownloadTableColumnKey[] | null>(null);
const columnDropFlashTimerRef = useRef<number | null>(null);
const suppressHeaderClickRef = useRef(false);
const selectedIdsRef = useRef(selectedIds);
@@ -152,45 +160,77 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
}
});
const [columnDragState, setColumnDragState] = useState<ColumnDragState | null>(null);
const [columnDragOrder, setColumnDragOrder] = useState<DownloadTableColumnKey[] | null>(null);
const [columnMenu, setColumnMenu] = useState<ColumnMenuState | null>(null);
const [columnDropFlashKey, setColumnDropFlashKey] = useState<DownloadTableColumnKey | null>(null);
const [, setMenuViewportVersion] = useState(0);
const columnWidthsRef = useRef(columnWidths);
const columnOrderRef = useRef(columnOrder);
columnWidthsRef.current = columnWidths;
columnOrderRef.current = columnOrder;
columnDragOrderRef.current = columnDragOrder;
const normalizedColumnWidths = columnWidths.map((width, index) =>
Math.max(COLUMN_MINIMUMS[index], width)
);
const orderedColumns = useMemo(() => columnOrder, [columnOrder]);
const orderedColumnWidths = orderedColumns.map(key => normalizedColumnWidths[columnIndex(key)]);
const tableGridTemplate = [
...orderedColumnWidths.slice(0, -1).map(width => `${width}px`),
'minmax(0, 1fr)',
`${orderedColumnWidths[orderedColumnWidths.length - 1]}px`
].join(' ');
const orderedColumns = columnDragOrder ?? columnOrder;
const tableGridTemplate = buildColumnGridTemplate(orderedColumns, normalizedColumnWidths);
const tableMinWidth = normalizedColumnWidths.reduce(
(total, width) => total + width,
0
);
const tableMinWidthWithPadding = 'calc(' + tableMinWidth + 'px + var(--download-row-padding-x) + var(--download-row-padding-x))';
const downloadsViewRef = useRef<HTMLDivElement>(null);
const updateColumnDragState = (next: ColumnDragState | null) => {
columnDragStateRef.current = next;
setColumnDragState(next);
};
const getColumnDropPosition = (key: DownloadTableColumnKey, pointerX: number) => {
const remainingColumns = orderedColumns.filter(columnKey => columnKey !== key);
const reorderColumn = (
order: DownloadTableColumnKey[],
key: DownloadTableColumnKey,
dropIndex: number
): DownloadTableColumnKey[] => {
const remainingColumns = order.filter(columnKey => columnKey !== key);
remainingColumns.splice(dropIndex, 0, key);
return remainingColumns;
};
const getColumnLayoutBounds = (element: HTMLDivElement) => {
// AutoAnimate transforms the visual box; offsetLeft/offsetWidth retain the
// stable grid geometry needed for hit-testing during a live reorder.
const container = headerContainerRef.current;
if (!container) {
const rect = element.getBoundingClientRect();
return { left: rect.left, right: rect.right };
}
const containerRect = container.getBoundingClientRect();
return {
left: containerRect.left + element.offsetLeft,
right: containerRect.left + element.offsetLeft + element.offsetWidth,
};
};
const getColumnDropPosition = (
key: DownloadTableColumnKey,
pointerX: number,
order: DownloadTableColumnKey[] = columnDragOrderRef.current ?? columnOrderRef.current
) => {
const remainingColumns = order.filter(columnKey => columnKey !== key);
const targetIndex = remainingColumns.findIndex(columnKey => {
const element = headerElementsRef.current.get(columnKey);
if (!element) return false;
const rect = element.getBoundingClientRect();
return pointerX < rect.left + rect.width / 2;
const bounds = getColumnLayoutBounds(element);
return pointerX < bounds.left + (bounds.right - bounds.left) / 2;
});
const dropIndex = targetIndex === -1 ? remainingColumns.length : targetIndex;
const markerElement = dropIndex < remainingColumns.length
? headerElementsRef.current.get(remainingColumns[dropIndex])
: headerElementsRef.current.get(remainingColumns[remainingColumns.length - 1]);
const markerRect = markerElement?.getBoundingClientRect();
const markerX = markerRect
? dropIndex < remainingColumns.length ? markerRect.left : markerRect.right
const markerBounds = markerElement ? getColumnLayoutBounds(markerElement) : null;
const markerX = markerBounds
? dropIndex < remainingColumns.length ? markerBounds.left : markerBounds.right
: pointerX;
return { dropIndex, markerX };
};
@@ -219,7 +259,10 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
gesture.active = true;
suppressHeaderClickRef.current = true;
const rect = event.currentTarget.getBoundingClientRect();
const { dropIndex, markerX } = getColumnDropPosition(key, event.clientX);
const currentOrder = columnOrderRef.current;
columnDragOrderRef.current = [...currentOrder];
setColumnDragOrder([...currentOrder]);
const { dropIndex, markerX } = getColumnDropPosition(key, event.clientX, currentOrder);
updateColumnDragState({
key,
startX: gesture.startX,
@@ -233,11 +276,18 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
} else {
const current = columnDragStateRef.current;
if (!current) return;
const { dropIndex, markerX } = getColumnDropPosition(key, event.clientX);
const currentOrder = columnDragOrderRef.current ?? columnOrderRef.current;
const { dropIndex, markerX } = getColumnDropPosition(key, event.clientX, currentOrder);
const nextOrder = reorderColumn(currentOrder, key, dropIndex);
const orderChanged = nextOrder.some((columnKey, index) => columnKey !== currentOrder[index]);
if (orderChanged) {
columnDragOrderRef.current = nextOrder;
setColumnDragOrder(nextOrder);
}
updateColumnDragState({
...current,
pointerX: event.clientX,
dropIndex,
dropIndex: nextOrder.indexOf(key),
markerX,
});
}
@@ -252,6 +302,9 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
const current = columnDragStateRef.current;
dragGestureRef.current = null;
updateColumnDragState(null);
const nextOrder = columnDragOrderRef.current ?? columnOrderRef.current;
columnDragOrderRef.current = null;
setColumnDragOrder(null);
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
event.currentTarget.releasePointerCapture(event.pointerId);
}
@@ -263,10 +316,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
suppressHeaderClickRef.current = false;
}, 0);
const remainingColumns = orderedColumns.filter(columnKey => columnKey !== key);
const nextOrder = [...remainingColumns];
nextOrder.splice(current.dropIndex, 0, key);
if (nextOrder.join('|') === orderedColumns.join('|')) return;
if (nextOrder.join('|') === columnOrderRef.current.join('|')) return;
setColumnOrder(nextOrder);
persistColumnOrder(nextOrder);
@@ -317,16 +367,30 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
document.addEventListener('visibilitychange', handlePointerUp);
};
const clampMenuPosition = useCallback((x: number, y: number, menuWidth: number, menuHeight: number) => {
const workspaceRect = downloadsViewRef.current?.getBoundingClientRect();
const minX = Math.max(8, (workspaceRect?.left ?? 0) + 8);
const minY = Math.max(8, (workspaceRect?.top ?? 0) + 8);
const maxX = Math.max(minX, Math.min(
window.innerWidth - menuWidth - 8,
(workspaceRect?.right ?? window.innerWidth) - menuWidth - 8
));
const maxY = Math.max(minY, Math.min(
window.innerHeight - menuHeight - 8,
(workspaceRect?.bottom ?? window.innerHeight) - menuHeight - 8
));
return {
x: Math.min(Math.max(minX, x), maxX),
y: Math.min(Math.max(minY, y), maxY),
};
}, []);
const openColumnMenu = (key: DownloadTableColumnKey, x: number, y: number) => {
const menuWidth = 188;
const menuHeight = 220;
const maxX = Math.max(8, window.innerWidth - menuWidth - 8);
const maxY = Math.max(8, window.innerHeight - menuHeight - 8);
const position = clampMenuPosition(x, y, 188, 220);
setContextMenu(null);
setColumnMenu({
key,
x: Math.min(Math.max(8, x), maxX),
y: Math.min(Math.max(8, y), maxY),
...position,
});
};
@@ -386,18 +450,29 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
if (!dragGestureRef.current) return;
dragGestureRef.current = null;
columnDragStateRef.current = null;
columnDragOrderRef.current = null;
setColumnDragState(null);
setColumnDragOrder(null);
suppressHeaderClickRef.current = false;
};
window.addEventListener('blur', cancelColumnDrag);
window.addEventListener('resize', cancelColumnDrag);
document.addEventListener('visibilitychange', cancelColumnDrag);
return () => {
window.removeEventListener('blur', cancelColumnDrag);
window.removeEventListener('resize', cancelColumnDrag);
document.removeEventListener('visibilitychange', cancelColumnDrag);
};
}, []);
useEffect(() => {
if (!columnMenu && !contextMenu) return;
const updateMenuViewport = () => setMenuViewportVersion(version => version + 1);
window.addEventListener('resize', updateMenuViewport);
return () => window.removeEventListener('resize', updateMenuViewport);
}, [columnMenu, contextMenu]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (document.querySelector('.app-modal-backdrop') || document.querySelector('.app-modal')) return;
@@ -610,8 +685,11 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
setLastSelectedId(menu.id);
}
setColumnMenu(null);
setContextMenu(menu);
}, []);
setContextMenu({
...menu,
...clampMenuPosition(menu.x, menu.y, 200, 300),
});
}, [clampMenuPosition]);
const handleMoveInQueue = useCallback((id: string, direction: 'up' | 'down') => {
const ids = selectedIdsRef.current.has(id)
@@ -772,9 +850,15 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
const columnAlignmentStyle = (key: DownloadTableColumnKey): React.CSSProperties => ({
'--column-justify': COLUMN_ALIGNMENT_JUSTIFY[columnAlignments[key]],
} as React.CSSProperties);
const columnMenuPosition = columnMenu
? clampMenuPosition(columnMenu.x, columnMenu.y, 188, 220)
: null;
const contextMenuPosition = contextMenu
? clampMenuPosition(contextMenu.x, contextMenu.y, 200, 300)
: null;
return (
<div className="downloads-view flex-1 flex flex-col h-full min-w-0">
<div ref={downloadsViewRef} className="downloads-view flex-1 flex flex-col h-full min-w-0">
<div
className="main-titlebar"
data-tauri-drag-region
@@ -839,8 +923,12 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
<div className="downloads-table flex-1 flex flex-col">
<div className="download-table-scroll">
<div
ref={element => {
headerContainerRef.current = element;
headerAnimationParent(element);
}}
className={`download-table-header ${columnDragState ? 'is-column-dragging' : ''}`}
style={{ gridTemplateColumns: tableGridTemplate, minWidth: tableMinWidth }}
style={{ gridTemplateColumns: tableGridTemplate, minWidth: tableMinWidthWithPadding }}
>
{orderedColumns.map(key => {
const label = columnLabels.get(key) ?? key;
@@ -860,7 +948,10 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
? activeSort.direction === 'asc' ? 'ascending' : 'descending'
: 'none'}
className={`download-column-header group ${isDragging ? 'is-dragging' : ''} ${isDropFlashing ? 'is-drop-flashing' : ''}`}
style={columnAlignmentStyle(key)}
style={{
...columnAlignmentStyle(key),
gridColumn: getColumnGridColumn(key, orderedColumns),
}}
onContextMenu={event => {
event.preventDefault();
event.stopPropagation();
@@ -942,7 +1033,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
</>
)}
<div className="download-table-body" style={{ minWidth: tableMinWidth }}>
<div className="download-table-body" style={{ minWidth: tableMinWidthWithPadding }}>
<div className="download-table-list" ref={animationParent}>
{sortedDownloads.length === 0 ? (
<div className="downloads-empty-state">
@@ -984,7 +1075,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
columnOrder={orderedColumns}
columnAlignments={columnAlignments}
tableGridTemplate={tableGridTemplate}
tableMinWidth={tableMinWidth}
tableMinWidth={tableMinWidthWithPadding}
setContextMenu={handleContextMenu}
handlePause={handlePause}
handleResume={handleResume}
@@ -1006,7 +1097,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
<div
role="menu"
className="download-column-menu app-modal fixed z-50 min-w-[188px] overflow-hidden py-1.5 text-[12px] font-medium text-text-primary"
style={{ top: columnMenu.y, left: columnMenu.x }}
style={{ top: columnMenuPosition?.y, left: columnMenuPosition?.x }}
onClick={event => event.stopPropagation()}
>
<div className="download-column-menu-title px-3 py-1.5 text-text-muted">
@@ -1050,8 +1141,8 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
role="menu"
className="app-modal fixed z-50 min-w-[180px] overflow-visible py-1.5 text-[12px] font-medium text-text-primary"
style={{
top: Math.min(Math.max(8, contextMenu.y), Math.max(8, window.innerHeight - 300)),
left: Math.min(Math.max(8, contextMenu.x), Math.max(8, window.innerWidth - 200))
top: contextMenuPosition?.y,
left: contextMenuPosition?.x,
}}
onClick={(e) => e.stopPropagation()}
>
+22 -21
View File
@@ -1887,6 +1887,7 @@ html[data-list-density="relaxed"] {
.download-table-header {
flex-shrink: 0;
position: relative;
height: var(--download-header-height);
display: grid;
align-items: center;
@@ -1926,7 +1927,6 @@ html[data-list-density="relaxed"] {
min-width: 0;
overflow: hidden;
padding-inline: var(--download-column-padding-x);
padding-inline-end: calc(var(--download-column-padding-x) + 30px);
cursor: grab;
transition: background-color 120ms ease, opacity 120ms ease, box-shadow 120ms ease;
}
@@ -1936,7 +1936,8 @@ html[data-list-density="relaxed"] {
}
.download-column-header.is-dragging {
opacity: 0.22;
opacity: 0;
pointer-events: none;
}
.download-column-header.is-drop-flashing {
@@ -1975,7 +1976,8 @@ html[data-list-density="relaxed"] {
.download-column-options {
position: absolute;
inset-inline-end: 8px;
right: 8px;
left: auto;
top: 50%;
z-index: 4;
display: inline-flex;
@@ -2008,15 +2010,6 @@ html[data-list-density="relaxed"] {
padding-inline-start: 0;
}
.download-column-header:last-child {
padding-inline-end: 30px;
}
.download-column-header:last-child,
.download-row > div:last-child {
grid-column: 7;
}
.download-column-header[data-column-key="File Name"] {
direction: ltr;
text-align: left;
@@ -2025,7 +2018,8 @@ html[data-list-density="relaxed"] {
.column-resize-handle {
position: absolute;
top: 0;
inset-inline-end: -4px;
right: -4px;
left: auto;
bottom: 0;
z-index: 5;
width: 8px;
@@ -2111,12 +2105,6 @@ html[data-list-density="relaxed"] .download-ghost-row {
padding-inline-start: 0;
}
.download-row > div:last-child {
padding-inline-end: 0;
}
.download-row.is-selected {
background: hsl(var(--accent-color) / 0.26) !important;
box-shadow: inset 0 0 0 1px hsl(var(--accent-color) / 0.28);
@@ -2138,6 +2126,10 @@ html[data-list-density="relaxed"] .download-ghost-row {
overflow: hidden;
}
.download-row:hover .download-column-cell--trailing .download-cell-content {
visibility: hidden;
}
.download-file-cell {
direction: ltr;
text-align: left;
@@ -2215,13 +2207,15 @@ html[data-list-density="relaxed"] .download-ghost-row {
.download-row-actions {
position: absolute;
top: 50%;
inset-inline-end: var(--download-row-padding-x);
right: var(--download-row-padding-x);
left: auto;
z-index: 2;
transform: translateY(-50%);
padding-inline-start: 4px;
border-radius: 6px;
background: hsl(var(--surface-overlay) / 0.94);
max-width: 100%;
padding-inline: 4px;
}
.download-column-menu {
@@ -2256,7 +2250,7 @@ html[data-list-density="relaxed"] .download-ghost-row {
font-size: var(--download-row-font-size);
font-weight: 650;
pointer-events: none;
transition: left 70ms ease-out, top 120ms ease-out, width 120ms ease-out;
transition: top 120ms ease-out, width 120ms ease-out;
animation: column-drag-preview-in 120ms ease-out;
}
@@ -2298,6 +2292,13 @@ html[data-list-density="relaxed"] .download-ghost-row {
margin-inline-start: 0.25rem;
}
.app-workspace--sidebar-right .download-context-submenu {
inset-inline-start: auto;
inset-inline-end: 100%;
margin-inline-start: 0;
margin-inline-end: 0.25rem;
}
html[dir="rtl"] .download-context-menu-chevron {
transform: scaleX(-1);
}
+13 -1
View File
@@ -2,6 +2,9 @@ import { describe, expect, it } from 'vitest';
import {
DEFAULT_COLUMN_ALIGNMENTS,
DEFAULT_COLUMN_ORDER,
DEFAULT_COLUMN_WIDTHS,
buildColumnGridTemplate,
getColumnGridColumn,
normalizeColumnAlignments,
normalizeColumnOrder,
normalizeColumnWidths,
@@ -26,7 +29,7 @@ describe('download table column preferences', () => {
220,
80,
80,
140,
144,
]);
expect(normalizeColumnWidths([0, 100, 220, 100, 80, 170])[0]).toBe(160);
@@ -50,4 +53,13 @@ describe('download table column preferences', () => {
expect(normalizeColumnOrder(null)).toEqual([...DEFAULT_COLUMN_ORDER]);
expect(normalizeColumnAlignments(null)).toEqual(DEFAULT_COLUMN_ALIGNMENTS);
});
it('keeps user widths fixed while reserving a shared trailing spacer track', () => {
expect(buildColumnGridTemplate([...DEFAULT_COLUMN_ORDER], [...DEFAULT_COLUMN_WIDTHS])).toBe(
'340px 100px 220px 100px 80px minmax(0, 1fr) 170px'
);
expect(getColumnGridColumn('Date Added', [...DEFAULT_COLUMN_ORDER])).toBe('7');
expect(getColumnGridColumn('Date Added', ['Date Added', ...DEFAULT_COLUMN_ORDER.slice(0, -1)])).toBeUndefined();
expect(getColumnGridColumn('ETA', ['Date Added', 'File Name', 'Size', 'Status', 'Speed', 'ETA'])).toBe('7');
});
});
+21 -1
View File
@@ -13,7 +13,7 @@ export const DEFAULT_COLUMN_ORDER = [
] as const satisfies readonly DownloadTableColumnKey[];
export const DEFAULT_COLUMN_WIDTHS = [340, 100, 220, 100, 80, 170] as const;
export const COLUMN_MINIMUMS = [160, 58, 92, 58, 48, 112] as const;
export const COLUMN_MINIMUMS = [160, 58, 92, 58, 48, 144] as const;
export const COLUMN_WIDTHS_STORAGE_KEY = 'firelink-download-column-widths';
export const COLUMN_ORDER_STORAGE_KEY = 'firelink-download-column-order';
@@ -87,3 +87,23 @@ export const normalizeColumnAlignments = (value: unknown): Record<DownloadTableC
export const columnIndex = (key: DownloadTableColumnKey): number =>
DEFAULT_COLUMN_ORDER.indexOf(key);
export const buildColumnGridTemplate = (
order: DownloadTableColumnKey[],
widths: number[]
): string => {
const normalizedWidths = normalizeColumnWidths(widths);
const orderedWidths = order.map(key => normalizedWidths[columnIndex(key)]);
return [
...orderedWidths.slice(0, -1).map(width => `${width}px`),
'minmax(0, 1fr)',
`${orderedWidths[orderedWidths.length - 1]}px`,
].join(' ');
};
export const getColumnGridColumn = (
key: DownloadTableColumnKey,
order: DownloadTableColumnKey[]
): string | undefined => key === order[order.length - 1]
? `${order.length + 1}`
: undefined;