mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
refactor(table): virtualise tables for perf for EditableTable components (#7810)
This commit is contained in:
@@ -1,10 +1,9 @@
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
const StyledWrapper = styled.div`
|
const StyledWrapper = styled.div`
|
||||||
display: flex;
|
display: block;
|
||||||
flex-direction: column;
|
width: 100%;
|
||||||
flex: 1;
|
isolation: isolate;
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&.is-resizing {
|
&.is-resizing {
|
||||||
cursor: col-resize !important;
|
cursor: col-resize !important;
|
||||||
@@ -12,9 +11,9 @@ const StyledWrapper = styled.div`
|
|||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.table-container {
|
||||||
overflow: auto;
|
|
||||||
border-radius: ${(props) => props.theme.border.radius.base};
|
border-radius: ${(props) => props.theme.border.radius.base};
|
||||||
border: solid 1px ${(props) => props.theme.border.border0};
|
border: solid 1px ${(props) => props.theme.border.border0};
|
||||||
|
overflow: clip;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
@@ -80,6 +79,8 @@ const StyledWrapper = styled.div`
|
|||||||
|
|
||||||
tbody {
|
tbody {
|
||||||
tr {
|
tr {
|
||||||
|
height: 35px;
|
||||||
|
max-height: 35px;
|
||||||
transition: background 0.1s ease;
|
transition: background 0.1s ease;
|
||||||
|
|
||||||
&:last-child td {
|
&:last-child td {
|
||||||
@@ -87,6 +88,8 @@ const StyledWrapper = styled.div`
|
|||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
|
height: 35px;
|
||||||
|
max-height: 35px;
|
||||||
padding: 1px 10px !important;
|
padding: 1px 10px !important;
|
||||||
border-top: none !important;
|
border-top: none !important;
|
||||||
border-left: none !important;
|
border-left: none !important;
|
||||||
@@ -96,17 +99,23 @@ const StyledWrapper = styled.div`
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
&:last-child {
|
> div:not(.drag-handle) {
|
||||||
border-right: none;
|
height: 33px;
|
||||||
|
max-height: 33px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Handle CodeMirror editors overflow */
|
/* Handle CodeMirror editors overflow */
|
||||||
.cm-editor {
|
.cm-editor {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
height: 33px !important;
|
||||||
|
max-height: 33px !important;
|
||||||
|
|
||||||
.cm-scroller {
|
.cm-scroller {
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
|
max-height: 33px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-content {
|
.cm-content {
|
||||||
@@ -185,12 +194,23 @@ const StyledWrapper = styled.div`
|
|||||||
}
|
}
|
||||||
|
|
||||||
.drag-handle {
|
.drag-handle {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.1s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
.icon-grip,
|
.icon-grip,
|
||||||
.icon-minus {
|
.icon-minus {
|
||||||
color: ${(props) => props.theme.colors.text.muted};
|
color: ${(props) => props.theme.colors.text.muted};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tbody tr:hover .drag-handle,
|
||||||
|
tbody tr.drag-over .drag-handle {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: ${(props) => props.theme.text};
|
color: ${(props) => props.theme.text};
|
||||||
|
|||||||
@@ -1,10 +1,49 @@
|
|||||||
import React, { useCallback, useMemo, useRef, useState, useEffect } from 'react';
|
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { TableVirtuoso } from 'react-virtuoso';
|
||||||
import { IconTrash, IconAlertCircle, IconGripVertical, IconMinusVertical } from '@tabler/icons';
|
import { IconTrash, IconAlertCircle, IconGripVertical, IconMinusVertical } from '@tabler/icons';
|
||||||
import { Tooltip } from 'react-tooltip';
|
import { Tooltip } from 'react-tooltip';
|
||||||
import { uuid } from 'utils/common';
|
import { uuid } from 'utils/common';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
const MIN_COLUMN_WIDTH = 80;
|
const MIN_COLUMN_WIDTH = 80;
|
||||||
|
const ROW_HEIGHT = 35;
|
||||||
|
|
||||||
|
const findScrollParent = (element) => {
|
||||||
|
let parent = element?.parentElement;
|
||||||
|
while (parent) {
|
||||||
|
const { overflowY } = getComputedStyle(parent);
|
||||||
|
if (overflowY === 'auto' || overflowY === 'scroll') return parent;
|
||||||
|
parent = parent.parentElement;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TableRow = React.memo(
|
||||||
|
({ children, item, context, ...rest }) => {
|
||||||
|
const rowIndex = Number(rest['data-item-index']);
|
||||||
|
const { reorderable, reorderableRowCount, isLastEmptyRow, dragOverRow, onDragStart, onDragOver, onDrop, onDragEnd, onDragLeave } = context;
|
||||||
|
const isEmpty = isLastEmptyRow(item, rowIndex);
|
||||||
|
const canDrag = reorderable && !isEmpty && rowIndex < reorderableRowCount;
|
||||||
|
const isDragOver = canDrag && dragOverRow === rowIndex;
|
||||||
|
const existingClass = rest.className || '';
|
||||||
|
const className = isDragOver ? `${existingClass} drag-over`.trim() : existingClass;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
{...rest}
|
||||||
|
className={className}
|
||||||
|
draggable={canDrag}
|
||||||
|
onDragStart={canDrag ? (e) => onDragStart(e, rowIndex) : undefined}
|
||||||
|
onDragOver={canDrag ? (e) => onDragOver(e, rowIndex) : undefined}
|
||||||
|
onDragLeave={canDrag ? (e) => onDragLeave(e, rowIndex) : undefined}
|
||||||
|
onDrop={canDrag ? (e) => onDrop(e, rowIndex) : undefined}
|
||||||
|
onDragEnd={canDrag ? onDragEnd : undefined}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const EditableTable = ({
|
const EditableTable = ({
|
||||||
tableId, // Not being used kept to maintain uniqueness & pass similar in onColumnWidthsChange
|
tableId, // Not being used kept to maintain uniqueness & pass similar in onColumnWidthsChange
|
||||||
@@ -25,13 +64,24 @@ const EditableTable = ({
|
|||||||
columnWidths,
|
columnWidths,
|
||||||
onColumnWidthsChange
|
onColumnWidthsChange
|
||||||
}) => {
|
}) => {
|
||||||
const tableRef = useRef(null);
|
const wrapperRef = useRef(null);
|
||||||
|
const virtuosoRef = useRef(null);
|
||||||
const emptyRowUidRef = useRef(null);
|
const emptyRowUidRef = useRef(null);
|
||||||
const [hoveredRow, setHoveredRow] = useState(null);
|
const prevRowCountRef = useRef(0);
|
||||||
const [resizing, setResizing] = useState(null);
|
const [resizing, setResizing] = useState(null);
|
||||||
const [tableHeight, setTableHeight] = useState(0);
|
const [tableHeight, setTableHeight] = useState(0);
|
||||||
|
const [scrollParent, setScrollParent] = useState(null);
|
||||||
|
const [dragOverRow, setDragOverRow] = useState(null);
|
||||||
const widths = columnWidths || {};
|
const widths = columnWidths || {};
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
setScrollParent(findScrollParent(wrapperRef.current));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleTotalHeightChanged = useCallback((h) => {
|
||||||
|
setTableHeight(h);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleColumnWidthsChange = useCallback((newWidths) => {
|
const handleColumnWidthsChange = useCallback((newWidths) => {
|
||||||
onColumnWidthsChange?.(newWidths);
|
onColumnWidthsChange?.(newWidths);
|
||||||
}, [onColumnWidthsChange]);
|
}, [onColumnWidthsChange]);
|
||||||
@@ -71,7 +121,7 @@ const EditableTable = ({
|
|||||||
|
|
||||||
const handleMouseUp = () => {
|
const handleMouseUp = () => {
|
||||||
// Convert pixel widths to percentages for responsive scaling
|
// Convert pixel widths to percentages for responsive scaling
|
||||||
const table = tableRef.current?.querySelector('table');
|
const table = wrapperRef.current?.querySelector('table');
|
||||||
if (table) {
|
if (table) {
|
||||||
const tableWidth = table.offsetWidth;
|
const tableWidth = table.offsetWidth;
|
||||||
const headerCells = table.querySelectorAll('thead td');
|
const headerCells = table.querySelectorAll('thead td');
|
||||||
@@ -103,23 +153,6 @@ const EditableTable = ({
|
|||||||
document.addEventListener('mouseup', handleMouseUp);
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
}, [columns, showCheckbox, widths, handleColumnWidthsChange]);
|
}, [columns, showCheckbox, widths, handleColumnWidthsChange]);
|
||||||
|
|
||||||
// Track table height for resize handles
|
|
||||||
useEffect(() => {
|
|
||||||
const table = tableRef.current?.querySelector('table');
|
|
||||||
if (!table) return;
|
|
||||||
|
|
||||||
const updateHeight = () => {
|
|
||||||
setTableHeight(table.offsetHeight);
|
|
||||||
};
|
|
||||||
|
|
||||||
updateHeight();
|
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(updateHeight);
|
|
||||||
resizeObserver.observe(table);
|
|
||||||
|
|
||||||
return () => resizeObserver.disconnect();
|
|
||||||
}, [rows.length]);
|
|
||||||
|
|
||||||
const getColumnWidth = useCallback((column) => {
|
const getColumnWidth = useCallback((column) => {
|
||||||
return widths[column.key] || column.width || 'auto';
|
return widths[column.key] || column.width || 'auto';
|
||||||
}, [widths]);
|
}, [widths]);
|
||||||
@@ -179,6 +212,16 @@ const EditableTable = ({
|
|||||||
return index === rowsWithEmpty.length - 1 && isEmptyRow(row);
|
return index === rowsWithEmpty.length - 1 && isEmptyRow(row);
|
||||||
}, [rowsWithEmpty.length, isEmptyRow, showAddRow]);
|
}, [rowsWithEmpty.length, isEmptyRow, showAddRow]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (rowsWithEmpty.length > prevRowCountRef.current && prevRowCountRef.current > 0) {
|
||||||
|
virtuosoRef.current?.scrollToIndex({
|
||||||
|
index: rowsWithEmpty.length - 1,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
prevRowCountRef.current = rowsWithEmpty.length;
|
||||||
|
}, [rowsWithEmpty.length]);
|
||||||
|
|
||||||
const handleValueChange = useCallback((rowUid, key, value) => {
|
const handleValueChange = useCallback((rowUid, key, value) => {
|
||||||
const rowIndex = rowsWithEmpty.findIndex((r) => r.uid === rowUid);
|
const rowIndex = rowsWithEmpty.findIndex((r) => r.uid === rowUid);
|
||||||
if (rowIndex === -1) return;
|
if (rowIndex === -1) return;
|
||||||
@@ -245,28 +288,31 @@ const EditableTable = ({
|
|||||||
const handleDragOver = useCallback((e, index) => {
|
const handleDragOver = useCallback((e, index) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.dataTransfer.dropEffect = 'move';
|
e.dataTransfer.dropEffect = 'move';
|
||||||
setHoveredRow(index);
|
setDragOverRow((prev) => (prev === index ? prev : index));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleDragLeave = useCallback((e, index) => {
|
||||||
|
if (e.currentTarget.contains(e.relatedTarget)) return;
|
||||||
|
setDragOverRow((prev) => (prev === index ? null : prev));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const reorderableRowCount = showAddRow ? rowsWithEmpty.length - 1 : rowsWithEmpty.length;
|
||||||
|
|
||||||
const handleDrop = useCallback((e, toIndex) => {
|
const handleDrop = useCallback((e, toIndex) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
setDragOverRow(null);
|
||||||
const fromIndex = parseInt(e.dataTransfer.getData('text/plain'), 10);
|
const fromIndex = parseInt(e.dataTransfer.getData('text/plain'), 10);
|
||||||
if (fromIndex !== toIndex && onReorder) {
|
if (fromIndex === toIndex || !onReorder) return;
|
||||||
const reorderableRows = showAddRow ? rowsWithEmpty.slice(0, -1) : rowsWithEmpty;
|
const reorderableRows = showAddRow ? rowsWithEmpty.slice(0, -1) : rowsWithEmpty;
|
||||||
const updatedOrder = [...reorderableRows];
|
const updatedOrder = [...reorderableRows];
|
||||||
const [movedRow] = updatedOrder.splice(fromIndex, 1);
|
const [movedRow] = updatedOrder.splice(fromIndex, 1);
|
||||||
if (!movedRow) {
|
if (!movedRow) return;
|
||||||
setHoveredRow(null);
|
updatedOrder.splice(toIndex, 0, movedRow);
|
||||||
return;
|
onReorder({ updateReorderedItem: updatedOrder.map((row) => row.uid) });
|
||||||
}
|
|
||||||
updatedOrder.splice(toIndex, 0, movedRow);
|
|
||||||
onReorder({ updateReorderedItem: updatedOrder.map((row) => row.uid) });
|
|
||||||
}
|
|
||||||
setHoveredRow(null);
|
|
||||||
}, [onReorder, rowsWithEmpty, showAddRow]);
|
}, [onReorder, rowsWithEmpty, showAddRow]);
|
||||||
|
|
||||||
const handleDragEnd = useCallback(() => {
|
const handleDragEnd = useCallback(() => {
|
||||||
setHoveredRow(null);
|
setDragOverRow(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const renderCell = useCallback((column, row, rowIndex) => {
|
const renderCell = useCallback((column, row, rowIndex) => {
|
||||||
@@ -323,109 +369,119 @@ const EditableTable = ({
|
|||||||
);
|
);
|
||||||
}, [isLastEmptyRow, getRowError, handleValueChange]);
|
}, [isLastEmptyRow, getRowError, handleValueChange]);
|
||||||
|
|
||||||
const reorderableRowCount = showAddRow ? rowsWithEmpty.length - 1 : rowsWithEmpty.length;
|
const virtuosoContext = useMemo(() => ({
|
||||||
|
reorderable,
|
||||||
|
reorderableRowCount,
|
||||||
|
isLastEmptyRow,
|
||||||
|
dragOverRow,
|
||||||
|
onDragStart: handleDragStart,
|
||||||
|
onDragOver: handleDragOver,
|
||||||
|
onDragLeave: handleDragLeave,
|
||||||
|
onDrop: handleDrop,
|
||||||
|
onDragEnd: handleDragEnd
|
||||||
|
}), [reorderable, reorderableRowCount, isLastEmptyRow, dragOverRow, handleDragStart, handleDragOver, handleDragLeave, handleDrop, handleDragEnd]);
|
||||||
|
|
||||||
|
const fixedHeaderContent = useCallback(() => (
|
||||||
|
<tr>
|
||||||
|
{showCheckbox && (
|
||||||
|
<td className="text-center">{checkboxLabel}</td>
|
||||||
|
)}
|
||||||
|
{columns.map((column, colIndex) => (
|
||||||
|
<td
|
||||||
|
key={column.key}
|
||||||
|
style={{ width: getColumnWidth(column) }}
|
||||||
|
>
|
||||||
|
<span className="column-name">{column.name}</span>
|
||||||
|
{colIndex < columns.length - 1 && (
|
||||||
|
<div
|
||||||
|
className={`resize-handle ${resizing === column.key ? 'resizing' : ''}`}
|
||||||
|
style={{ height: tableHeight > 0 ? `${tableHeight}px` : undefined }}
|
||||||
|
onMouseDown={(e) => handleResizeStart(e, column.key)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
{showDelete && (
|
||||||
|
<td style={{ width: '60px' }}></td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
), [showCheckbox, checkboxLabel, columns, getColumnWidth, resizing, tableHeight, handleResizeStart, showDelete]);
|
||||||
|
|
||||||
|
const itemContent = useCallback((rowIndex, row) => {
|
||||||
|
const isEmpty = isLastEmptyRow(row, rowIndex);
|
||||||
|
const canDrag = reorderable && !isEmpty && rowIndex < reorderableRowCount;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showCheckbox && (
|
||||||
|
<td className="text-center relative">
|
||||||
|
{reorderable && canDrag && (
|
||||||
|
<div
|
||||||
|
draggable
|
||||||
|
className="drag-handle group absolute z-10 left-[-8px] top-1/2 -translate-y-1/2 p-1 cursor-grab"
|
||||||
|
>
|
||||||
|
<IconGripVertical
|
||||||
|
size={14}
|
||||||
|
className="icon-grip hidden group-hover:block"
|
||||||
|
/>
|
||||||
|
<IconMinusVertical
|
||||||
|
size={14}
|
||||||
|
className="icon-minus block group-hover:hidden"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isEmpty && (
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="mousetrap"
|
||||||
|
data-testid="column-checkbox"
|
||||||
|
checked={row[checkboxKey] ?? true}
|
||||||
|
disabled={disableCheckbox}
|
||||||
|
onChange={(e) => handleCheckboxChange(row.uid, e.target.checked)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
{columns.map((column) => (
|
||||||
|
<td key={column.key} data-testid={`column-${column.key}`}>
|
||||||
|
{renderCell(column, row, rowIndex)}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
{showDelete && (
|
||||||
|
<td>
|
||||||
|
{!isEmpty && (
|
||||||
|
<button
|
||||||
|
data-testid="column-delete"
|
||||||
|
onClick={() => handleRemoveRow(row.uid)}
|
||||||
|
>
|
||||||
|
<IconTrash strokeWidth={1.5} size={18} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}, [showCheckbox, reorderable, reorderableRowCount, isLastEmptyRow, checkboxKey, disableCheckbox, handleCheckboxChange, columns, renderCell, showDelete, handleRemoveRow]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper className={`${showCheckbox ? 'has-checkbox' : 'no-checkbox'} ${resizing ? 'is-resizing' : ''}`}>
|
<StyledWrapper
|
||||||
<div className="table-container" ref={tableRef} data-testid={testId}>
|
ref={wrapperRef}
|
||||||
<table>
|
data-testid={testId}
|
||||||
<thead>
|
className={`${showCheckbox ? 'has-checkbox' : 'no-checkbox'} ${resizing ? 'is-resizing' : ''}`}
|
||||||
<tr>
|
>
|
||||||
{showCheckbox && (
|
<TableVirtuoso
|
||||||
<td className="text-center">{checkboxLabel}</td>
|
ref={virtuosoRef}
|
||||||
)}
|
className="table-container"
|
||||||
{columns.map((column, colIndex) => (
|
customScrollParent={scrollParent || undefined}
|
||||||
<td
|
data={rowsWithEmpty}
|
||||||
key={column.key}
|
components={{ TableRow }}
|
||||||
style={{ width: getColumnWidth(column) }}
|
context={virtuosoContext}
|
||||||
>
|
defaultItemHeight={ROW_HEIGHT}
|
||||||
<span className="column-name">{column.name}</span>
|
totalListHeightChanged={handleTotalHeightChanged}
|
||||||
{colIndex < columns.length - 1 && (
|
computeItemKey={(_, item) => item.uid}
|
||||||
<div
|
fixedHeaderContent={fixedHeaderContent}
|
||||||
className={`resize-handle ${resizing === column.key ? 'resizing' : ''}`}
|
itemContent={itemContent}
|
||||||
style={{ height: tableHeight > 0 ? `${tableHeight}px` : undefined }}
|
/>
|
||||||
onMouseDown={(e) => handleResizeStart(e, column.key)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
{showDelete && (
|
|
||||||
<td style={{ width: '60px' }}></td>
|
|
||||||
)}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{rowsWithEmpty.map((row, rowIndex) => {
|
|
||||||
const isEmpty = isLastEmptyRow(row, rowIndex);
|
|
||||||
const canDrag = reorderable && !isEmpty && rowIndex < reorderableRowCount;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr
|
|
||||||
key={row.uid}
|
|
||||||
draggable={canDrag}
|
|
||||||
onDragStart={canDrag ? (e) => handleDragStart(e, rowIndex) : undefined}
|
|
||||||
onDragOver={canDrag ? (e) => handleDragOver(e, rowIndex) : undefined}
|
|
||||||
onDrop={canDrag ? (e) => handleDrop(e, rowIndex) : undefined}
|
|
||||||
onDragEnd={canDrag ? handleDragEnd : undefined}
|
|
||||||
onMouseEnter={() => setHoveredRow(rowIndex)}
|
|
||||||
onMouseLeave={() => setHoveredRow(null)}
|
|
||||||
>
|
|
||||||
{showCheckbox && (
|
|
||||||
<td className="text-center relative">
|
|
||||||
{reorderable && canDrag && (
|
|
||||||
<div
|
|
||||||
draggable
|
|
||||||
className="drag-handle group absolute z-10 left-[-8px] top-1/2 -translate-y-1/2 p-1 cursor-grab"
|
|
||||||
>
|
|
||||||
{hoveredRow === rowIndex && (
|
|
||||||
<>
|
|
||||||
<IconGripVertical
|
|
||||||
size={14}
|
|
||||||
className="icon-grip hidden group-hover:block"
|
|
||||||
/>
|
|
||||||
<IconMinusVertical
|
|
||||||
size={14}
|
|
||||||
className="icon-minus block group-hover:hidden"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!isEmpty && (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="mousetrap"
|
|
||||||
data-testid="column-checkbox"
|
|
||||||
checked={row[checkboxKey] ?? true}
|
|
||||||
disabled={disableCheckbox}
|
|
||||||
onChange={(e) => handleCheckboxChange(row.uid, e.target.checked)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
{columns.map((column) => (
|
|
||||||
<td key={column.key} data-testid={`column-${column.key}`}>
|
|
||||||
{renderCell(column, row, rowIndex)}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
{showDelete && (
|
|
||||||
<td>
|
|
||||||
{!isEmpty && (
|
|
||||||
<button
|
|
||||||
data-testid="column-delete"
|
|
||||||
onClick={() => handleRemoveRow(row.uid)}
|
|
||||||
>
|
|
||||||
<IconTrash strokeWidth={1.5} size={18} />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</StyledWrapper>
|
</StyledWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,6 +38,14 @@ const Wrapper = styled.div`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bulk-edit-bar {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
background: ${(props) => props.theme.bg};
|
||||||
|
padding-top: 8px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
input[type='text'] {
|
input[type='text'] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: solid 1px transparent;
|
border: solid 1px transparent;
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ const QueryParams = ({ item, collection }) => {
|
|||||||
columnWidths={queryParamsWidths}
|
columnWidths={queryParamsWidths}
|
||||||
onColumnWidthsChange={(widths) => handleColumnWidthsChange('query-params', widths)}
|
onColumnWidthsChange={(widths) => handleColumnWidthsChange('query-params', widths)}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-end mt-2">
|
<div className="bulk-edit-bar flex justify-end mt-2">
|
||||||
<button className="btn-action text-link select-none" onClick={toggleBulkEditMode}>
|
<button className="btn-action text-link select-none" onClick={toggleBulkEditMode}>
|
||||||
Bulk Edit
|
Bulk Edit
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -29,6 +29,14 @@ const Wrapper = styled.div`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bulk-edit-bar {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
background: ${(props) => props.theme.bg};
|
||||||
|
padding-top: 8px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
input[type='text'] {
|
input[type='text'] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: solid 1px transparent;
|
border: solid 1px transparent;
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ const RequestHeaders = ({ item, collection, addHeaderText }) => {
|
|||||||
columnWidths={headersWidths}
|
columnWidths={headersWidths}
|
||||||
onColumnWidthsChange={(widths) => handleColumnWidthsChange('request-headers', widths)}
|
onColumnWidthsChange={(widths) => handleColumnWidthsChange('request-headers', widths)}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-end mt-2">
|
<div className="bulk-edit-bar flex justify-end mt-2">
|
||||||
<button className="btn-action text-link select-none" onClick={toggleBulkEditMode}>
|
<button className="btn-action text-link select-none" onClick={toggleBulkEditMode}>
|
||||||
Bulk Edit
|
Bulk Edit
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user