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:
Pragadesh-45 2024-09-05 12:19:36 +05:30 committed by GitHub
parent 4bd31fb083
commit 2191550061
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 392 additions and 73 deletions

View 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;

View File

@ -22,14 +22,12 @@ const Wrapper = styled.div`
}
td {
padding: 6px 10px;
}
}
&:nth-child(1) {
width: 30%;
}
&:nth-child(3) {
width: 70px;
}
td {
&:nth-child(1) {
padding: 0 0 0 8px;
}
}

View File

@ -7,14 +7,17 @@ import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import {
addQueryParam,
updateQueryParam,
deleteQueryParam,
updatePathParam,
updateQueryParam
moveQueryParam,
updatePathParam
} from 'providers/ReduxStore/slices/collections';
import SingleLineEditor from 'components/SingleLineEditor';
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import Table from 'components/Table/index';
import ReorderTable from 'components/ReorderTable';
const QueryParams = ({ item, collection }) => {
const dispatch = useDispatch();
@ -100,76 +103,75 @@ const QueryParams = ({ item, collection }) => {
);
};
const handleParamDrag = ({ updateReorderedItem }) => {
dispatch(
moveQueryParam({
collectionUid: collection.uid,
itemUid: item.uid,
updateReorderedItem
})
);
};
return (
<StyledWrapper className="w-full flex flex-col">
<StyledWrapper className="w-full flex flex-col absolute">
<div className="flex-1 mt-2">
<div className="mb-2 title text-xs">Query</div>
<table>
<thead>
<tr>
<td>Name</td>
<td>Value</td>
<td></td>
</tr>
</thead>
<tbody>
<div className="mb-1 title text-xs">Query</div>
<Table
headers={[
{ name: 'Name', accessor: 'name', width: '31%' },
{ name: 'Path', accessor: 'path', width: '56%' },
{ name: '', accessor: '', width: '13%' }
]}
>
<ReorderTable updateReorderedItem={handleParamDrag}>
{queryParams && queryParams.length
? queryParams.map((param, index) => {
return (
<tr key={param.uid}>
<td>
? queryParams.map((param, index) => (
<tr key={param.uid} data-uid={param.uid}>
<td className="flex relative">
<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
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={param.name}
className="mousetrap"
onChange={(e) => handleQueryParamChange(e, param, 'name')}
type="checkbox"
checked={param.enabled}
tabIndex="-1"
className="mr-3 mousetrap"
onChange={(e) => handleQueryParamChange(e, param, 'enabled')}
/>
</td>
<td>
<SingleLineEditor
value={param.value}
theme={storedTheme}
onSave={onSave}
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>
);
})
<button tabIndex="-1" onClick={() => handleRemoveQueryParam(param)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
))
: null}
</tbody>
</table>
</ReorderTable>
</Table>
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={handleAddQueryParam}>
+&nbsp;<span>Add Param</span>
</button>

View 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;

View 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;

View File

@ -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) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
@ -1720,6 +1753,7 @@ export const {
requestUrlChanged,
updateAuth,
addQueryParam,
moveQueryParam,
updateQueryParam,
deleteQueryParam,
updatePathParam,