diff --git a/packages/bruno-app/src/components/EditableTable/StyledWrapper.js b/packages/bruno-app/src/components/EditableTable/StyledWrapper.js
index 6f0c79426..c15cde52a 100644
--- a/packages/bruno-app/src/components/EditableTable/StyledWrapper.js
+++ b/packages/bruno-app/src/components/EditableTable/StyledWrapper.js
@@ -1,10 +1,9 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
- display: flex;
- flex-direction: column;
- flex: 1;
- overflow: hidden;
+ display: block;
+ width: 100%;
+ isolation: isolate;
&.is-resizing {
cursor: col-resize !important;
@@ -12,9 +11,9 @@ const StyledWrapper = styled.div`
}
.table-container {
- overflow: auto;
border-radius: ${(props) => props.theme.border.radius.base};
border: solid 1px ${(props) => props.theme.border.border0};
+ overflow: clip;
}
table {
@@ -80,6 +79,8 @@ const StyledWrapper = styled.div`
tbody {
tr {
+ height: 35px;
+ max-height: 35px;
transition: background 0.1s ease;
&:last-child td {
@@ -87,6 +88,8 @@ const StyledWrapper = styled.div`
}
td {
+ height: 35px;
+ max-height: 35px;
padding: 1px 10px !important;
border-top: none !important;
border-left: none !important;
@@ -96,17 +99,23 @@ const StyledWrapper = styled.div`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
+ box-sizing: border-box;
- &:last-child {
- border-right: none;
+ > div:not(.drag-handle) {
+ height: 33px;
+ max-height: 33px;
+ overflow: hidden;
}
/* Handle CodeMirror editors overflow */
.cm-editor {
max-width: 100%;
+ height: 33px !important;
+ max-height: 33px !important;
.cm-scroller {
overflow: hidden !important;
+ max-height: 33px;
}
.cm-content {
@@ -185,12 +194,23 @@ const StyledWrapper = styled.div`
}
.drag-handle {
+ opacity: 0;
+ transition: opacity 0.1s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
.icon-grip,
.icon-minus {
color: ${(props) => props.theme.colors.text.muted};
}
}
+ tbody tr:hover .drag-handle,
+ tbody tr.drag-over .drag-handle {
+ opacity: 1;
+ }
+
select {
background-color: transparent;
color: ${(props) => props.theme.text};
diff --git a/packages/bruno-app/src/components/EditableTable/index.js b/packages/bruno-app/src/components/EditableTable/index.js
index 67c5886c8..abf75d84d 100644
--- a/packages/bruno-app/src/components/EditableTable/index.js
+++ b/packages/bruno-app/src/components/EditableTable/index.js
@@ -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 { Tooltip } from 'react-tooltip';
import { uuid } from 'utils/common';
import StyledWrapper from './StyledWrapper';
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 (
+
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}
+
+ );
+ }
+);
const EditableTable = ({
tableId, // Not being used kept to maintain uniqueness & pass similar in onColumnWidthsChange
@@ -25,13 +64,24 @@ const EditableTable = ({
columnWidths,
onColumnWidthsChange
}) => {
- const tableRef = useRef(null);
+ const wrapperRef = useRef(null);
+ const virtuosoRef = useRef(null);
const emptyRowUidRef = useRef(null);
- const [hoveredRow, setHoveredRow] = useState(null);
+ const prevRowCountRef = useRef(0);
const [resizing, setResizing] = useState(null);
const [tableHeight, setTableHeight] = useState(0);
+ const [scrollParent, setScrollParent] = useState(null);
+ const [dragOverRow, setDragOverRow] = useState(null);
const widths = columnWidths || {};
+ useLayoutEffect(() => {
+ setScrollParent(findScrollParent(wrapperRef.current));
+ }, []);
+
+ const handleTotalHeightChanged = useCallback((h) => {
+ setTableHeight(h);
+ }, []);
+
const handleColumnWidthsChange = useCallback((newWidths) => {
onColumnWidthsChange?.(newWidths);
}, [onColumnWidthsChange]);
@@ -71,7 +121,7 @@ const EditableTable = ({
const handleMouseUp = () => {
// Convert pixel widths to percentages for responsive scaling
- const table = tableRef.current?.querySelector('table');
+ const table = wrapperRef.current?.querySelector('table');
if (table) {
const tableWidth = table.offsetWidth;
const headerCells = table.querySelectorAll('thead td');
@@ -103,23 +153,6 @@ const EditableTable = ({
document.addEventListener('mouseup', handleMouseUp);
}, [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) => {
return widths[column.key] || column.width || 'auto';
}, [widths]);
@@ -179,6 +212,16 @@ const EditableTable = ({
return index === rowsWithEmpty.length - 1 && isEmptyRow(row);
}, [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 rowIndex = rowsWithEmpty.findIndex((r) => r.uid === rowUid);
if (rowIndex === -1) return;
@@ -245,28 +288,31 @@ const EditableTable = ({
const handleDragOver = useCallback((e, index) => {
e.preventDefault();
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) => {
e.preventDefault();
+ setDragOverRow(null);
const fromIndex = parseInt(e.dataTransfer.getData('text/plain'), 10);
- if (fromIndex !== toIndex && onReorder) {
- const reorderableRows = showAddRow ? rowsWithEmpty.slice(0, -1) : rowsWithEmpty;
- const updatedOrder = [...reorderableRows];
- const [movedRow] = updatedOrder.splice(fromIndex, 1);
- if (!movedRow) {
- setHoveredRow(null);
- return;
- }
- updatedOrder.splice(toIndex, 0, movedRow);
- onReorder({ updateReorderedItem: updatedOrder.map((row) => row.uid) });
- }
- setHoveredRow(null);
+ if (fromIndex === toIndex || !onReorder) return;
+ const reorderableRows = showAddRow ? rowsWithEmpty.slice(0, -1) : rowsWithEmpty;
+ const updatedOrder = [...reorderableRows];
+ const [movedRow] = updatedOrder.splice(fromIndex, 1);
+ if (!movedRow) return;
+ updatedOrder.splice(toIndex, 0, movedRow);
+ onReorder({ updateReorderedItem: updatedOrder.map((row) => row.uid) });
}, [onReorder, rowsWithEmpty, showAddRow]);
const handleDragEnd = useCallback(() => {
- setHoveredRow(null);
+ setDragOverRow(null);
}, []);
const renderCell = useCallback((column, row, rowIndex) => {
@@ -323,109 +369,119 @@ const EditableTable = ({
);
}, [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(() => (
+
+ {showCheckbox && (
+ | {checkboxLabel} |
+ )}
+ {columns.map((column, colIndex) => (
+
+ {column.name}
+ {colIndex < columns.length - 1 && (
+ 0 ? `${tableHeight}px` : undefined }}
+ onMouseDown={(e) => handleResizeStart(e, column.key)}
+ />
+ )}
+ |
+ ))}
+ {showDelete && (
+ |
+ )}
+
+ ), [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 && (
+
+ {reorderable && canDrag && (
+
+
+
+
+ )}
+ {!isEmpty && (
+ handleCheckboxChange(row.uid, e.target.checked)}
+ />
+ )}
+ |
+ )}
+ {columns.map((column) => (
+
+ {renderCell(column, row, rowIndex)}
+ |
+ ))}
+ {showDelete && (
+
+ {!isEmpty && (
+
+ )}
+ |
+ )}
+ >
+ );
+ }, [showCheckbox, reorderable, reorderableRowCount, isLastEmptyRow, checkboxKey, disableCheckbox, handleCheckboxChange, columns, renderCell, showDelete, handleRemoveRow]);
return (
-
-
-
-
-
- {showCheckbox && (
- | {checkboxLabel} |
- )}
- {columns.map((column, colIndex) => (
-
- {column.name}
- {colIndex < columns.length - 1 && (
- 0 ? `${tableHeight}px` : undefined }}
- onMouseDown={(e) => handleResizeStart(e, column.key)}
- />
- )}
- |
- ))}
- {showDelete && (
- |
- )}
-
-
-
- {rowsWithEmpty.map((row, rowIndex) => {
- const isEmpty = isLastEmptyRow(row, rowIndex);
- const canDrag = reorderable && !isEmpty && rowIndex < reorderableRowCount;
-
- return (
- 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 && (
- |
- {reorderable && canDrag && (
-
- {hoveredRow === rowIndex && (
- <>
-
-
- >
- )}
-
- )}
- {!isEmpty && (
- handleCheckboxChange(row.uid, e.target.checked)}
- />
- )}
- |
- )}
- {columns.map((column) => (
-
- {renderCell(column, row, rowIndex)}
- |
- ))}
- {showDelete && (
-
- {!isEmpty && (
-
- )}
- |
- )}
-
- );
- })}
-
-
-
+
+ item.uid}
+ fixedHeaderContent={fixedHeaderContent}
+ itemContent={itemContent}
+ />
);
};
diff --git a/packages/bruno-app/src/components/RequestPane/QueryParams/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/QueryParams/StyledWrapper.js
index 38ef5cc3d..ed140d79c 100644
--- a/packages/bruno-app/src/components/RequestPane/QueryParams/StyledWrapper.js
+++ b/packages/bruno-app/src/components/RequestPane/QueryParams/StyledWrapper.js
@@ -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'] {
width: 100%;
border: solid 1px transparent;
diff --git a/packages/bruno-app/src/components/RequestPane/QueryParams/index.js b/packages/bruno-app/src/components/RequestPane/QueryParams/index.js
index 1cd6c2cc7..01165d014 100644
--- a/packages/bruno-app/src/components/RequestPane/QueryParams/index.js
+++ b/packages/bruno-app/src/components/RequestPane/QueryParams/index.js
@@ -160,7 +160,7 @@ const QueryParams = ({ item, collection }) => {
columnWidths={queryParamsWidths}
onColumnWidthsChange={(widths) => handleColumnWidthsChange('query-params', widths)}
/>
-
+
diff --git a/packages/bruno-app/src/components/RequestPane/RequestHeaders/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/RequestHeaders/StyledWrapper.js
index 8a47eb68e..bb4aa404e 100644
--- a/packages/bruno-app/src/components/RequestPane/RequestHeaders/StyledWrapper.js
+++ b/packages/bruno-app/src/components/RequestPane/RequestHeaders/StyledWrapper.js
@@ -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'] {
width: 100%;
border: solid 1px transparent;
diff --git a/packages/bruno-app/src/components/RequestPane/RequestHeaders/index.js b/packages/bruno-app/src/components/RequestPane/RequestHeaders/index.js
index f23bafbae..939a9b3bd 100644
--- a/packages/bruno-app/src/components/RequestPane/RequestHeaders/index.js
+++ b/packages/bruno-app/src/components/RequestPane/RequestHeaders/index.js
@@ -145,7 +145,7 @@ const RequestHeaders = ({ item, collection, addHeaderText }) => {
columnWidths={headersWidths}
onColumnWidthsChange={(widths) => handleColumnWidthsChange('request-headers', widths)}
/>
-