Files
bruno/packages/bruno-app/src/components/RequestPane/Assertions/index.js
Sid 0bba66a590 feat: variable descriptions (#8269)
* 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>
2026-07-07 21:43:39 +05:30

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;