diff --git a/packages/bruno-app/src/components/ReorderTable/index.js b/packages/bruno-app/src/components/ReorderTable/index.js new file mode 100644 index 000000000..9d8c11088 --- /dev/null +++ b/packages/bruno-app/src/components/ReorderTable/index.js @@ -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 ( + + {rowsOrder.map((row, index) => ( + 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: ( + <> +
+ {hoveredRow === index && ( + <> + + + + )} +
+ {child.props.children} + + ) + }); + } else { + return child; + } + })} + + ))} + + ); +}; + +export default ReorderTable; diff --git a/packages/bruno-app/src/components/RequestPane/QueryParams/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/QueryParams/StyledWrapper.js index 5c3e1d537..b460c1b4f 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryParams/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/QueryParams/StyledWrapper.js @@ -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; } } diff --git a/packages/bruno-app/src/components/RequestPane/QueryParams/index.js b/packages/bruno-app/src/components/RequestPane/QueryParams/index.js index 3d94dd3ca..a1099f4fd 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryParams/index.js +++ b/packages/bruno-app/src/components/RequestPane/QueryParams/index.js @@ -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 ( - +
-
Query
- - - - - - - - - +
Query
+ +
NameValue
+ {queryParams && queryParams.length - ? queryParams.map((param, index) => { - return ( - - + + + - - - - ); - }) + + + + + )) : null} - -
+ ? queryParams.map((param, index) => ( +
+ handleQueryParamChange(e, param, 'name')} + /> + + handleQueryParamChange({ target: { value: newValue } }, param, 'value')} + onRun={handleRun} + collection={collection} + variablesAutocomplete={true} + /> + +
handleQueryParamChange(e, param, 'name')} + type="checkbox" + checked={param.enabled} + tabIndex="-1" + className="mr-3 mousetrap" + onChange={(e) => handleQueryParamChange(e, param, 'enabled')} /> -
- - handleQueryParamChange( - { - target: { - value: newValue - } - }, - param, - 'value' - ) - } - onRun={handleRun} - collection={collection} - item={item} - /> - -
- handleQueryParamChange(e, param, 'enabled')} - /> - -
-
+ + + diff --git a/packages/bruno-app/src/components/Table/StyledWrapper.js b/packages/bruno-app/src/components/Table/StyledWrapper.js new file mode 100644 index 000000000..e35f11b3a --- /dev/null +++ b/packages/bruno-app/src/components/Table/StyledWrapper.js @@ -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; diff --git a/packages/bruno-app/src/components/Table/index.js b/packages/bruno-app/src/components/Table/index.js new file mode 100644 index 000000000..80bfb19f3 --- /dev/null +++ b/packages/bruno-app/src/components/Table/index.js @@ -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 ( + +
+ + + + {columns.map(({ ref, name }, i) => ( + + ))} + + + {children} +
+ {name} +
+
+
+
+ ); +}; + +export default Table; diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index 63e49360c..e8fb4d602 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -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,