mirror of
https://github.com/usebruno/bruno.git
synced 2024-11-25 01:14:23 +01:00
Feat table resize and reorder (#2641)
* feat-Table-resize-and-Reorder * feat-Table-resize-and-Reorder * feat-Table-resize-and-Reorder/fixed-table-resize-update
This commit is contained in:
parent
4bd31fb083
commit
2191550061
112
packages/bruno-app/src/components/ReorderTable/index.js
Normal file
112
packages/bruno-app/src/components/ReorderTable/index.js
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||||
|
import { IconGripVertical, IconMinusVertical } from '@tabler/icons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ReorderTable Component
|
||||||
|
*
|
||||||
|
* A table component that allows rows to be reordered via drag-and-drop.
|
||||||
|
*
|
||||||
|
* @param {Object} props - The component props
|
||||||
|
* @param {React.ReactNode[]} props.children - The table rows as children
|
||||||
|
* @param {function} props.updateReorderedItem - Callback function to handle reordered rows
|
||||||
|
*/
|
||||||
|
|
||||||
|
const ReorderTable = ({ children, updateReorderedItem }) => {
|
||||||
|
const tbodyRef = useRef();
|
||||||
|
const [rowsOrder, setRowsOrder] = useState(React.Children.toArray(children));
|
||||||
|
const [hoveredRow, setHoveredRow] = useState(null);
|
||||||
|
const [dragStart, setDragStart] = useState(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* useEffect hook to update the rows order and handle row hover states
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
setRowsOrder(React.Children.toArray(children));
|
||||||
|
handleRowHover(null, false);
|
||||||
|
}, [children, dragStart]);
|
||||||
|
|
||||||
|
const handleRowHover = (index, hoverstatus = true) => {
|
||||||
|
setHoveredRow(hoverstatus ? index : null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragStart = (e, index) => {
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
e.dataTransfer.setData('text/plain', index);
|
||||||
|
setDragStart(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (e, index) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = 'move';
|
||||||
|
handleRowHover(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (e, toIndex) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const fromIndex = parseInt(e.dataTransfer.getData('text/plain'), 10);
|
||||||
|
if (fromIndex !== toIndex) {
|
||||||
|
const updatedRowsOrder = [...rowsOrder];
|
||||||
|
const [movedRow] = updatedRowsOrder.splice(fromIndex, 1);
|
||||||
|
updatedRowsOrder.splice(toIndex, 0, movedRow);
|
||||||
|
setRowsOrder(updatedRowsOrder);
|
||||||
|
|
||||||
|
updateReorderedItem({
|
||||||
|
updateReorderedItem: updatedRowsOrder.map((row) => row.props['data-uid'])
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
handleRowHover(toIndex);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tbody ref={tbodyRef}>
|
||||||
|
{rowsOrder.map((row, index) => (
|
||||||
|
<tr
|
||||||
|
key={row.props['data-uid']}
|
||||||
|
data-uid={row.props['data-uid']}
|
||||||
|
draggable
|
||||||
|
onDragStart={(e) => handleDragStart(e, index)}
|
||||||
|
onDragOver={(e) => handleDragOver(e, index)}
|
||||||
|
onDrop={(e) => handleDrop(e, index)}
|
||||||
|
onMouseEnter={() => handleRowHover(index)}
|
||||||
|
onMouseLeave={() => handleRowHover(index, false)}
|
||||||
|
>
|
||||||
|
{React.Children.map(row.props.children, (child, childIndex) => {
|
||||||
|
if (childIndex === 0) {
|
||||||
|
return React.cloneElement(child, {
|
||||||
|
children: (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
draggable
|
||||||
|
className="group drag-handle absolute z-10 left-[-17px] p-3.5 py-3.5 px-2.5 top-[3px] cursor-grab"
|
||||||
|
>
|
||||||
|
{hoveredRow === index && (
|
||||||
|
<>
|
||||||
|
<IconGripVertical
|
||||||
|
size={14}
|
||||||
|
className="z-10 icon-grip rounded-md absolute hidden group-hover:block"
|
||||||
|
/>
|
||||||
|
<IconMinusVertical
|
||||||
|
size={14}
|
||||||
|
className="z-10 icon-minus rounded-md absolute block group-hover:hidden"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{child.props.children}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReorderTable;
|
@ -22,14 +22,12 @@ const Wrapper = styled.div`
|
|||||||
}
|
}
|
||||||
td {
|
td {
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:nth-child(1) {
|
td {
|
||||||
width: 30%;
|
&:nth-child(1) {
|
||||||
}
|
padding: 0 0 0 8px;
|
||||||
|
|
||||||
&:nth-child(3) {
|
|
||||||
width: 70px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,14 +7,17 @@ import { useDispatch } from 'react-redux';
|
|||||||
import { useTheme } from 'providers/Theme';
|
import { useTheme } from 'providers/Theme';
|
||||||
import {
|
import {
|
||||||
addQueryParam,
|
addQueryParam,
|
||||||
|
updateQueryParam,
|
||||||
deleteQueryParam,
|
deleteQueryParam,
|
||||||
updatePathParam,
|
moveQueryParam,
|
||||||
updateQueryParam
|
updatePathParam
|
||||||
} from 'providers/ReduxStore/slices/collections';
|
} from 'providers/ReduxStore/slices/collections';
|
||||||
import SingleLineEditor from 'components/SingleLineEditor';
|
import SingleLineEditor from 'components/SingleLineEditor';
|
||||||
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
|
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
import Table from 'components/Table/index';
|
||||||
|
import ReorderTable from 'components/ReorderTable';
|
||||||
|
|
||||||
const QueryParams = ({ item, collection }) => {
|
const QueryParams = ({ item, collection }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@ -100,76 +103,75 @@ const QueryParams = ({ item, collection }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleParamDrag = ({ updateReorderedItem }) => {
|
||||||
|
dispatch(
|
||||||
|
moveQueryParam({
|
||||||
|
collectionUid: collection.uid,
|
||||||
|
itemUid: item.uid,
|
||||||
|
updateReorderedItem
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper className="w-full flex flex-col">
|
<StyledWrapper className="w-full flex flex-col absolute">
|
||||||
<div className="flex-1 mt-2">
|
<div className="flex-1 mt-2">
|
||||||
<div className="mb-2 title text-xs">Query</div>
|
<div className="mb-1 title text-xs">Query</div>
|
||||||
<table>
|
|
||||||
<thead>
|
<Table
|
||||||
<tr>
|
headers={[
|
||||||
<td>Name</td>
|
{ name: 'Name', accessor: 'name', width: '31%' },
|
||||||
<td>Value</td>
|
{ name: 'Path', accessor: 'path', width: '56%' },
|
||||||
<td></td>
|
{ name: '', accessor: '', width: '13%' }
|
||||||
</tr>
|
]}
|
||||||
</thead>
|
>
|
||||||
<tbody>
|
<ReorderTable updateReorderedItem={handleParamDrag}>
|
||||||
{queryParams && queryParams.length
|
{queryParams && queryParams.length
|
||||||
? queryParams.map((param, index) => {
|
? queryParams.map((param, index) => (
|
||||||
return (
|
<tr key={param.uid} data-uid={param.uid}>
|
||||||
<tr key={param.uid}>
|
<td className="flex relative">
|
||||||
<td>
|
<input
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoCapitalize="off"
|
||||||
|
spellCheck="false"
|
||||||
|
value={param.name}
|
||||||
|
className="mousetrap"
|
||||||
|
onChange={(e) => handleQueryParamChange(e, param, 'name')}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<SingleLineEditor
|
||||||
|
value={param.value}
|
||||||
|
theme={storedTheme}
|
||||||
|
onSave={onSave}
|
||||||
|
onChange={(newValue) => handleQueryParamChange({ target: { value: newValue } }, param, 'value')}
|
||||||
|
onRun={handleRun}
|
||||||
|
collection={collection}
|
||||||
|
variablesAutocomplete={true}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="flex items-center">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="checkbox"
|
||||||
autoComplete="off"
|
checked={param.enabled}
|
||||||
autoCorrect="off"
|
tabIndex="-1"
|
||||||
autoCapitalize="off"
|
className="mr-3 mousetrap"
|
||||||
spellCheck="false"
|
onChange={(e) => handleQueryParamChange(e, param, 'enabled')}
|
||||||
value={param.name}
|
|
||||||
className="mousetrap"
|
|
||||||
onChange={(e) => handleQueryParamChange(e, param, 'name')}
|
|
||||||
/>
|
/>
|
||||||
</td>
|
<button tabIndex="-1" onClick={() => handleRemoveQueryParam(param)}>
|
||||||
<td>
|
<IconTrash strokeWidth={1.5} size={20} />
|
||||||
<SingleLineEditor
|
</button>
|
||||||
value={param.value}
|
</div>
|
||||||
theme={storedTheme}
|
</td>
|
||||||
onSave={onSave}
|
</tr>
|
||||||
onChange={(newValue) =>
|
))
|
||||||
handleQueryParamChange(
|
|
||||||
{
|
|
||||||
target: {
|
|
||||||
value: newValue
|
|
||||||
}
|
|
||||||
},
|
|
||||||
param,
|
|
||||||
'value'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onRun={handleRun}
|
|
||||||
collection={collection}
|
|
||||||
item={item}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={param.enabled}
|
|
||||||
tabIndex="-1"
|
|
||||||
className="mr-3 mousetrap"
|
|
||||||
onChange={(e) => handleQueryParamChange(e, param, 'enabled')}
|
|
||||||
/>
|
|
||||||
<button tabIndex="-1" onClick={() => handleRemoveQueryParam(param)}>
|
|
||||||
<IconTrash strokeWidth={1.5} size={20} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
: null}
|
: null}
|
||||||
</tbody>
|
</ReorderTable>
|
||||||
</table>
|
</Table>
|
||||||
|
|
||||||
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={handleAddQueryParam}>
|
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={handleAddQueryParam}>
|
||||||
+ <span>Add Param</span>
|
+ <span>Add Param</span>
|
||||||
</button>
|
</button>
|
||||||
|
63
packages/bruno-app/src/components/Table/StyledWrapper.js
Normal file
63
packages/bruno-app/src/components/Table/StyledWrapper.js
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const StyledWrapper = styled.div`
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
overflow-y: hidden;
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
// for icon hover
|
||||||
|
position: inherit;
|
||||||
|
left: -4px;
|
||||||
|
padding-left: 4px;
|
||||||
|
padding-right: 4px;
|
||||||
|
|
||||||
|
grid-template-columns: ${({ columns }) =>
|
||||||
|
columns?.[0]?.width
|
||||||
|
? columns.map((col) => `${col?.width}`).join(' ')
|
||||||
|
: columns.map((col) => `${100 / columns.length}%`).join(' ')};
|
||||||
|
}
|
||||||
|
|
||||||
|
table thead,
|
||||||
|
table tbody,
|
||||||
|
table tr {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
table th {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tr td {
|
||||||
|
padding: 0.5rem;
|
||||||
|
text-align: left;
|
||||||
|
border-top: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
|
||||||
|
border-right: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr {
|
||||||
|
transition: transform 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.dragging {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.hovered {
|
||||||
|
transform: translateY(10px); /* Adjust the value as needed for the animation effect */
|
||||||
|
}
|
||||||
|
|
||||||
|
table tr th {
|
||||||
|
padding: 0.5rem;
|
||||||
|
text-align: left;
|
||||||
|
border-top: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
|
||||||
|
border-right: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
|
||||||
|
|
||||||
|
&:nth-child(1) {
|
||||||
|
border-left: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default StyledWrapper;
|
110
packages/bruno-app/src/components/Table/index.js
Normal file
110
packages/bruno-app/src/components/Table/index.js
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import { useState, useRef, useEffect, useCallback } from 'react';
|
||||||
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
|
const Table = ({ minColumnWidth = 1, headers = [], children }) => {
|
||||||
|
const [activeColumnIndex, setActiveColumnIndex] = useState(null);
|
||||||
|
const tableRef = useRef(null);
|
||||||
|
|
||||||
|
const columns = headers?.map((item) => ({
|
||||||
|
...item,
|
||||||
|
ref: useRef()
|
||||||
|
}));
|
||||||
|
|
||||||
|
const updateDivHeights = () => {
|
||||||
|
if (tableRef.current) {
|
||||||
|
const height = tableRef.current.offsetHeight;
|
||||||
|
columns.forEach((col) => {
|
||||||
|
if (col.ref.current) {
|
||||||
|
col.ref.current.querySelector('.resizer').style.height = `${height}px`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateDivHeights();
|
||||||
|
window.addEventListener('resize', updateDivHeights);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', updateDivHeights);
|
||||||
|
};
|
||||||
|
}, [columns]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (tableRef.current) {
|
||||||
|
const observer = new MutationObserver(updateDivHeights);
|
||||||
|
observer.observe(tableRef.current, { childList: true, subtree: true });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [columns]);
|
||||||
|
|
||||||
|
const handleMouseDown = (index) => (e) => {
|
||||||
|
setActiveColumnIndex(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = useCallback(
|
||||||
|
(e) => {
|
||||||
|
const gridColumns = columns.map((col, i) => {
|
||||||
|
if (i === activeColumnIndex) {
|
||||||
|
const width = e.clientX - col.ref?.current?.getBoundingClientRect()?.left;
|
||||||
|
|
||||||
|
if (width >= minColumnWidth) {
|
||||||
|
return `${width}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `${col.ref.current.offsetWidth}px`;
|
||||||
|
});
|
||||||
|
|
||||||
|
tableRef.current.style.gridTemplateColumns = `${gridColumns.join(' ')}`;
|
||||||
|
},
|
||||||
|
[activeColumnIndex, columns, minColumnWidth]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMouseUp = useCallback(() => {
|
||||||
|
setActiveColumnIndex(null);
|
||||||
|
removeListeners();
|
||||||
|
}, [removeListeners]);
|
||||||
|
|
||||||
|
const removeListeners = useCallback(() => {
|
||||||
|
window.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
window.removeEventListener('mouseup', removeListeners);
|
||||||
|
}, [handleMouseMove]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeColumnIndex !== null) {
|
||||||
|
window.addEventListener('mousemove', handleMouseMove);
|
||||||
|
window.addEventListener('mouseup', handleMouseUp);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
removeListeners();
|
||||||
|
};
|
||||||
|
}, [activeColumnIndex, handleMouseMove, handleMouseUp, removeListeners]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledWrapper columns={columns}>
|
||||||
|
<div className="relative">
|
||||||
|
<table ref={tableRef} className="px-4 inherit left-[4px]">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{columns.map(({ ref, name }, i) => (
|
||||||
|
<th ref={ref} key={name} title={name}>
|
||||||
|
<span>{name}</span>
|
||||||
|
<div
|
||||||
|
className="resizer absolute cursor-col-resize w-[4px] right-[-2px] top-0 z-10 opacity-50 hover:bg-blue-500 active:bg-blue-500"
|
||||||
|
onMouseDown={handleMouseDown(i)}
|
||||||
|
></div>
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{children}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</StyledWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Table;
|
@ -503,6 +503,39 @@ export const collectionsSlice = createSlice({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
moveQueryParam: (state, action) => {
|
||||||
|
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||||
|
|
||||||
|
if (collection) {
|
||||||
|
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||||
|
|
||||||
|
if (item && isItemARequest(item)) {
|
||||||
|
// Ensure item.draft is a deep clone of item if not already present
|
||||||
|
if (!item.draft) {
|
||||||
|
item.draft = cloneDeep(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract payload data
|
||||||
|
const { updateReorderedItem } = action.payload;
|
||||||
|
const params = item.draft.request.params;
|
||||||
|
|
||||||
|
item.draft.request.params = updateReorderedItem.map((uid) => {
|
||||||
|
return params.find((param) => param.uid === uid);
|
||||||
|
});
|
||||||
|
|
||||||
|
// update request url
|
||||||
|
const parts = splitOnFirst(item.draft.request.url, '?');
|
||||||
|
const query = stringifyQueryParams(filter(item.draft.request.params, (p) => p.enabled && p.type === 'query'));
|
||||||
|
if (query && query.length) {
|
||||||
|
item.draft.request.url = parts[0] + '?' + query;
|
||||||
|
} else {
|
||||||
|
item.draft.request.url = parts[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
updateQueryParam: (state, action) => {
|
updateQueryParam: (state, action) => {
|
||||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||||
|
|
||||||
@ -1720,6 +1753,7 @@ export const {
|
|||||||
requestUrlChanged,
|
requestUrlChanged,
|
||||||
updateAuth,
|
updateAuth,
|
||||||
addQueryParam,
|
addQueryParam,
|
||||||
|
moveQueryParam,
|
||||||
updateQueryParam,
|
updateQueryParam,
|
||||||
deleteQueryParam,
|
deleteQueryParam,
|
||||||
updatePathParam,
|
updatePathParam,
|
||||||
|
Loading…
Reference in New Issue
Block a user