mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-09 06:55:03 +00:00
* feat: add description column to various tables and enhance UI interactions - Introduced a description column in Headers, VarsTable, and other components to allow multi-line descriptions. - Added toggle buttons to show/hide the description column in relevant tables. - Updated EditableTable component to support dynamic row addition with customizable labels. - Enhanced UI for better user experience with consistent button styles and spacing adjustments. * chore: resize fix handles * fix: restrict column check for last index * refactor: remove unwanted style and fix bottom padding of the last child in the table row * tests: improve testability and fix impacted locators * tests: fix locators for value fields * fix: improve newline handling in description prefix * fix: ts changes * chore: remove redundant code and fix description roundtrip * chore: remove redundant check * tests(bru-lang): tests for description and annotation * test: improve locators and tests * tests: descriptions * chore: re-add description setup * fix: re-add description cols * chore: fix the double click reset * fix: account for hidden sidebar in request pane width calculation * refactor: ui/ux fixes for data type toggle * chore: inline common deps into bruno-lang and bruno-schema * chore: tests * chore: fix ux for descriptions * fix: layout for environment vars table * fix: ensure correct row selection for multipart file upload in tests * feat: enhance UID assignment for request headers and variables in collections * chore: simplify * chore: update pkg * chore: abstract the e2e utils * chore: fix exports * tests: fix locators for datatype vars * fix: ux issue with secrets in environment table * tests: update locators for collection headers and vars descriptions * tests: update locators to use data attributes for datatype selectors * chore: update default css width for actions column * chore: remove tooltip for string type warnings * fix: reduce name widths * refactor: update annotation serialization to use single-quote delimiters for descriptions * refactor(tests): simplify description formatting in jsonToEnv tests * feat: add multiline description escaping and unescaping functions with tests * refactor: clean up imports and improve multiline text block handling * refactor: update locators to use new test IDs for environment variable editors * refactor: streamline header input handling and improve environment variable row access * refactor: update e2e-test job to support self-hosted runners * refactor: update E2E test runner configuration to include e2e and macOS environments --------- Co-authored-by: Pragadesh-45 <temporaryg7904@gmail.com>
205 lines
6.3 KiB
JavaScript
205 lines
6.3 KiB
JavaScript
import React, { useCallback, useRef } from 'react';
|
|
import get from 'lodash/get';
|
|
import { useDispatch, useSelector } from 'react-redux';
|
|
import { useTheme } from 'providers/Theme';
|
|
import { moveAssertion, setRequestAssertions } from 'providers/ReduxStore/slices/collections';
|
|
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
|
import { updateTableColumnWidths } from 'providers/ReduxStore/slices/tabs';
|
|
import SingleLineEditor from 'components/SingleLineEditor';
|
|
import AssertionOperator from './AssertionOperator';
|
|
import EditableTable from 'components/EditableTable';
|
|
import { createDescriptionColumn } from 'components/EditableTable/descriptionColumn';
|
|
import StyledWrapper from './StyledWrapper';
|
|
import { usePersistedState } from 'hooks/usePersistedState';
|
|
import { useTrackScroll } from 'hooks/useTrackScroll';
|
|
|
|
const unaryOperators = [
|
|
'isEmpty',
|
|
'isNotEmpty',
|
|
'isNull',
|
|
'isUndefined',
|
|
'isDefined',
|
|
'isTruthy',
|
|
'isFalsy',
|
|
'isJson',
|
|
'isNumber',
|
|
'isString',
|
|
'isBoolean',
|
|
'isArray'
|
|
];
|
|
|
|
const parseAssertionOperator = (str = '') => {
|
|
if (!str || typeof str !== 'string' || !str.length) {
|
|
return { operator: 'eq', value: str };
|
|
}
|
|
|
|
const operators = [
|
|
'eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'in', 'notIn',
|
|
'contains', 'notContains', 'length', 'matches', 'notMatches',
|
|
'startsWith', 'endsWith', 'between', ...unaryOperators
|
|
];
|
|
|
|
const [operator, ...rest] = str.split(' ');
|
|
const value = rest.join(' ');
|
|
|
|
if (unaryOperators.includes(operator)) {
|
|
return { operator, value: '' };
|
|
}
|
|
|
|
if (operators.includes(operator)) {
|
|
return { operator, value };
|
|
}
|
|
|
|
return { operator: 'eq', value: str };
|
|
};
|
|
|
|
const isUnaryOperator = (operator) => unaryOperators.includes(operator);
|
|
|
|
const Assertions = ({ item, collection }) => {
|
|
const dispatch = useDispatch();
|
|
const { storedTheme } = useTheme();
|
|
const wrapperRef = useRef(null);
|
|
const [scroll, setScroll] = usePersistedState({ key: `request-assert-scroll-${item.uid}`, default: 0 });
|
|
useTrackScroll({ ref: wrapperRef, selector: '.flex-boundary', onChange: setScroll, initialValue: scroll });
|
|
const tabs = useSelector((state) => state.tabs.tabs);
|
|
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
|
const assertions = item.draft ? get(item, 'draft.request.assertions') : get(item, 'request.assertions');
|
|
|
|
// Get column widths from Redux
|
|
const focusedTab = tabs?.find((t) => t.uid === activeTabUid);
|
|
const assertionsWidths = focusedTab?.tableColumnWidths?.['assertions'] || {};
|
|
|
|
const handleColumnWidthsChange = (tableId, widths) => {
|
|
dispatch(updateTableColumnWidths({ uid: activeTabUid, tableId, widths }));
|
|
};
|
|
|
|
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
|
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
|
|
|
const handleAssertionsChange = useCallback((updatedAssertions) => {
|
|
dispatch(setRequestAssertions({
|
|
collectionUid: collection.uid,
|
|
itemUid: item.uid,
|
|
assertions: updatedAssertions
|
|
}));
|
|
}, [dispatch, collection.uid, item.uid]);
|
|
|
|
const handleAssertionDrag = useCallback(({ updateReorderedItem }) => {
|
|
dispatch(moveAssertion({
|
|
collectionUid: collection.uid,
|
|
itemUid: item.uid,
|
|
updateReorderedItem
|
|
}));
|
|
}, [dispatch, collection.uid, item.uid]);
|
|
|
|
const descriptionColumn = createDescriptionColumn({
|
|
theme: storedTheme,
|
|
onSave,
|
|
onRun: handleRun,
|
|
collection,
|
|
item,
|
|
nameFromRowIndex: true
|
|
});
|
|
|
|
const columns = [
|
|
{
|
|
key: 'name',
|
|
name: 'Expr',
|
|
isKeyField: true,
|
|
placeholder: 'Expr',
|
|
width: '20%'
|
|
},
|
|
{
|
|
key: 'operator',
|
|
name: 'Operator',
|
|
width: '120px',
|
|
getValue: (row) => parseAssertionOperator(row.value).operator,
|
|
render: ({ row, rowIndex, isLastEmptyRow }) => {
|
|
const { operator } = parseAssertionOperator(row.value);
|
|
const assertionValue = parseAssertionOperator(row.value).value;
|
|
|
|
const handleOperatorChange = (newOperator) => {
|
|
const currentAssertions = assertions || [];
|
|
const existingAssertion = currentAssertions.find((a) => a.uid === row.uid);
|
|
const newValue = isUnaryOperator(newOperator) ? newOperator : `${newOperator} ${assertionValue}`;
|
|
|
|
if (existingAssertion) {
|
|
const updatedAssertions = currentAssertions.map((assertion) => {
|
|
if (assertion.uid === row.uid) {
|
|
return {
|
|
...assertion,
|
|
value: newValue
|
|
};
|
|
}
|
|
return assertion;
|
|
});
|
|
handleAssertionsChange(updatedAssertions);
|
|
} else {
|
|
handleAssertionsChange([...currentAssertions, { ...row, value: newValue }]);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<AssertionOperator
|
|
operator={operator}
|
|
onChange={handleOperatorChange}
|
|
/>
|
|
);
|
|
}
|
|
},
|
|
{
|
|
key: 'value',
|
|
name: 'Value',
|
|
width: '30%',
|
|
render: ({ row, value, onChange }) => {
|
|
const { operator, value: assertionValue } = parseAssertionOperator(value);
|
|
|
|
if (isUnaryOperator(operator)) {
|
|
return <input type="text" className="cursor-default" disabled />;
|
|
}
|
|
|
|
return (
|
|
<SingleLineEditor
|
|
value={assertionValue}
|
|
theme={storedTheme}
|
|
onSave={onSave}
|
|
onChange={(newValue) => onChange(`${operator} ${newValue}`)}
|
|
onRun={handleRun}
|
|
collection={collection}
|
|
item={item}
|
|
placeholder={!value ? 'Value' : ''}
|
|
/>
|
|
);
|
|
}
|
|
},
|
|
descriptionColumn
|
|
];
|
|
|
|
const defaultRow = {
|
|
name: '',
|
|
value: 'eq ',
|
|
operator: 'eq',
|
|
description: ''
|
|
};
|
|
|
|
return (
|
|
<StyledWrapper className="w-full" ref={wrapperRef}>
|
|
<EditableTable
|
|
tableId="assertions"
|
|
columns={columns}
|
|
rows={assertions || []}
|
|
onChange={handleAssertionsChange}
|
|
defaultRow={defaultRow}
|
|
reorderable={true}
|
|
onReorder={handleAssertionDrag}
|
|
testId="assertions-table"
|
|
columnWidths={assertionsWidths}
|
|
onColumnWidthsChange={(widths) => handleColumnWidthsChange('assertions', widths)}
|
|
initialScroll={scroll}
|
|
/>
|
|
</StyledWrapper>
|
|
);
|
|
};
|
|
|
|
export default Assertions;
|