From 53201b5de3a9faae72bff452aec26e1793f8d3f4 Mon Sep 17 00:00:00 2001 From: sanish-bruno Date: Thu, 18 Sep 2025 16:24:16 +0530 Subject: [PATCH 1/4] feat: enhance Bru grammar to support response blocks and examples - Added new grammar rules for response headers, status, and body types (JSON, XML, text). - Introduced parsing logic for example blocks, allowing multiple examples with various body types. - Implemented tests for example parsing, including edge cases and complex examples with authentication. - Created fixture files for simple and complex examples to validate parsing functionality. feat: extend jsonToBru functionality to support response handling and examples - Updated jsonToBru to include parsing for response headers, status, and body types (JSON, XML, text). - Enhanced example handling to support multiple examples with various body types. - Added comprehensive tests for example parsing, including edge cases and complex scenarios with authentication. - Created fixture files for testing the new features and validating parsing functionality. move: files to fixtures folder refactor: simplify response body handling in Bru grammar and JSON conversion - Removed specific body type handling (JSON, XML, text) from grammar and semantics. - Updated response body parsing in jsonToBru to handle a unified response body format. - Adjusted tests and fixtures to reflect changes in response body structure, ensuring compatibility with the new format. feat: add response bookmarking functionality to ResponsePane - Introduced ResponseBookmark component to allow users to save responses as examples. - Added NameExampleModal for naming saved examples. - Updated ResponsePane to include the new bookmarking feature. - Implemented Redux actions to manage response examples in the collections state. - Enhanced CollectionItem to display saved examples and allow for expansion. fix: remove unnecessary padding from ExampleItem component feat: implement delete and rename functionality for examples in ExampleItem component - Added DeleteExampleModal for confirming deletion of examples. - Integrated modal for renaming examples with state management. - Enhanced ExampleItem to handle example deletion and renaming through modals. - Updated Redux actions to support example updates and deletions in the collections state. fix: example writing to disc properly fix: example parsing errors fix: request with example parsing error fix: handle examples in collections and requests feat: implement response example functionality in the application - Added ResponseExample component to handle displaying and editing response examples. - Integrated ResponseExampleRequestPane and ResponseExampleResponsePane for structured request and response handling. - Enhanced RequestTabPanel and RequestTab components to support response-example tabs. - Introduced new styled components for better UI/UX in response examples. - Updated theme files to include styles for response examples. - Implemented URL bar for editing request URLs in response examples. - Added functionality for managing headers and parameters in response examples. - Improved overall structure and organization of response example components. add styles for example url bar feat: add Checkbox component and Table-v2 for enhanced UI - Introduced a new Checkbox component for better user interaction in forms. - Added Table-v2 component to improve table rendering and resizing functionality. - Updated existing components to utilize the new Checkbox and Table-v2 for managing headers and parameters in response examples. - Enhanced styling for better visual consistency across components. - Updated theme files to include styles for the new components. feat: implement custom scrollbar styles for response example components fix: features add actions , view more feat: enhance response example functionality - Added GenerateCodeItem component for generating code snippets from response examples. - Integrated modal for code generation within ResponseExample component. - Updated ResponseExampleTopBar to handle example name and description editing. - Improved state management for response examples, including new actions for updating names and descriptions. - Enhanced ResponseExampleRequestPane to support editing and saving request details. - Refactored URL handling in ResponseExampleUrlBar to utilize example-specific data. - Improved overall user experience with better UI elements and state management. feat: enhance response example management and UI components feat: enhance editing capabilities in response example components feat: update multipart form parameter handling in response examples feat: refactor response example parameter handling and enhance UI interactions feat: introduce RadioButton component and update Checkbox usage in response examples fix: styles fix radio button styling fixed radio button styles feat: add create example from sidebar feat: enhance ResponseExample components with layout adjustments and new HeightBoundContainer feat: add Checkbox and RadioButton components with comprehensive tests for rendering, user interactions, and accessibility feat: playwright test csaes rm: comments fix: linting fix: tests refactor: update response example tests and enhance functionality fix: tests fix: e2e-tests refactor: implement hasRequestChanges utility for better change detection rm: console rm: consoles fix: lint fix: tests --- .../src/components/Checkbox/StyledWrapper.js | 79 ++ .../src/components/Checkbox/index.js | 45 + .../src/components/Checkbox/index.spec.js | 226 ++++ .../src/components/CodeEditor/index.js | 4 + .../src/components/FilePickerEditor/index.js | 18 +- .../src/components/Icons/examples/index.js | 59 + .../bruno-app/src/components/Modal/index.js | 9 +- .../src/components/MultiLineEditor/index.js | 5 + .../components/RadioButton/StyledWrapper.js | 74 ++ .../src/components/RadioButton/index.js | 40 + .../src/components/RadioButton/index.spec.js | 347 ++++++ .../components/RequestPane/QueryUrl/index.js | 7 +- .../RequestPane/WsQueryUrl/index.js | 7 +- .../src/components/RequestTabPanel/index.js | 27 + .../RequestTab/ConfirmRequestClose/index.js | 8 +- .../RequestTabs/RequestTab/index.js | 88 +- .../CreateExampleModal/index.js | 84 ++ .../ResponseExampleBody/StyledWrapper.js | 80 ++ .../ResponseExampleBody/index.js | 77 ++ .../ResponseExampleBodyMode/index.js | 186 +++ .../ResponseExampleBodyRenderer/index.js | 104 ++ .../StyledWrapper.js | 38 + .../ResponseExampleDescription/index.js | 52 + .../ResponseExampleFileBody/StyledWrapper.js | 131 +++ .../ResponseExampleFileBody/index.js | 221 ++++ .../StyledWrapper.js | 80 ++ .../index.js | 178 +++ .../ResponseExampleHeaders/StyledWrapper.js | 60 + .../ResponseExampleHeaders/index.js | 206 ++++ .../StyledWrapper.js | 100 ++ .../index.js | 262 +++++ .../ResponseExampleParams/StyledWrapper.js | 89 ++ .../ResponseExampleParams/index.js | 192 +++ .../ResponseExampleUrlBar/StyledWrapper.js | 53 + .../ResponseExampleUrlBar/index.js | 76 ++ .../StyledWrapper.js | 31 + .../ResponseExampleRequestPane/index.js | 47 + .../StyledWrapper.js | 16 + .../ResponseExampleResponseContent/index.js | 72 ++ .../StyledWrapper.js | 56 + .../ResponseExampleResponseHeaders/index.js | 200 ++++ .../StyledWrapper.js | 39 + .../ResponseExampleResponsePane/index.js | 91 ++ .../ResponseExampleTopBar/StyledWrapper.js | 94 ++ .../ResponseExampleTopBar/index.js | 200 ++++ .../ResponseExample/StyledWrapper.js | 67 ++ .../src/components/ResponseExample/index.js | 208 ++++ .../ResponseBookmark/StyledWrapper.js | 8 + .../ResponsePane/ResponseBookmark/index.js | 81 ++ .../src/components/ResponsePane/index.js | 2 + .../ExampleItem/DeleteExampleModal.js | 37 + .../ExampleItem/StyledWrapper.js | 49 + .../CollectionItem/ExampleItem/index.js | 206 ++++ .../CollectionItem/GenerateCodeItem/index.js | 71 +- .../Collection/CollectionItem/index.js | 92 +- .../src/components/SingleLineEditor/index.js | 5 + .../src/components/Table-v2/StyledWrapper.js | 77 ++ .../src/components/Table-v2/index.js | 109 ++ .../src/components/TruncatedText/index.js | 108 ++ packages/bruno-app/src/globalStyles.js | 36 + .../App/ConfirmAppClose/SaveRequestsModal.js | 4 +- .../src/providers/ReduxStore/index.js | 1 + .../slices/collections/exampleReducers.js | 1037 +++++++++++++++++ .../ReduxStore/slices/collections/index.js | 84 +- .../src/providers/ReduxStore/slices/tabs.js | 8 +- packages/bruno-app/src/themes/dark.js | 20 + packages/bruno-app/src/themes/light.js | 20 + .../src/ui/HeightBoundContainer/index.js | 4 +- .../bruno-app/src/utils/collections/index.js | 82 +- packages/bruno-cli/src/utils/bru.js | 3 +- .../bruno-electron/src/cache/requestUids.js | 15 +- packages/bruno-electron/src/ipc/collection.js | 1 - .../bruno-electron/src/utils/collection.js | 20 +- .../bruno-filestore/src/formats/bru/index.ts | 76 +- packages/bruno-filestore/src/index.ts | 3 +- .../bruno-filestore/src/types/bruno-lang.d.ts | 2 +- packages/bruno-lang/v2/src/bruToJson.js | 82 +- packages/bruno-lang/v2/src/jsonToBru.js | 44 +- .../v2/tests/examples/examples.spec.js | 414 +++++++ .../fixtures/bruToJson-basic-auth.bru | 20 + .../fixtures/bruToJson-basic-auth.json | 26 + .../fixtures/bruToJson-bearer-auth.bru | 19 + .../fixtures/bruToJson-bearer-auth.json | 25 + .../fixtures/bruToJson-empty-example.bru | 8 + .../fixtures/bruToJson-empty-example.json | 10 + .../examples/fixtures/bruToJson-json-body.bru | 22 + .../fixtures/bruToJson-json-body.json | 23 + .../fixtures/bruToJson-multiple-examples.bru | 36 + .../fixtures/bruToJson-multiple-examples.json | 38 + .../fixtures/bruToJson-no-examples.bru | 8 + .../fixtures/bruToJson-no-examples.json | 11 + .../fixtures/bruToJson-response-example.bru | 34 + .../fixtures/bruToJson-response-example.json | 37 + .../fixtures/bruToJson-single-example.bru | 23 + .../fixtures/bruToJson-single-example.json | 31 + .../examples/fixtures/bruToJson-text-body.bru | 20 + .../fixtures/bruToJson-text-body.json | 23 + .../examples/fixtures/bruToJson-xml-body.bru | 23 + .../examples/fixtures/bruToJson-xml-body.json | 23 + .../examples/fixtures/examples-complex.bru | 142 +++ .../examples/fixtures/examples-simple.bru | 71 ++ .../examples/fixtures/examples-simple.json | 69 ++ .../examples/fixtures/jsonToBru-auth.bru | 41 + .../examples/fixtures/jsonToBru-auth.json | 43 + .../examples/fixtures/jsonToBru-bodytypes.bru | 64 + .../fixtures/jsonToBru-bodytypes.json | 51 + .../examples/fixtures/jsonToBru-multiple.bru | 38 + .../examples/fixtures/jsonToBru-multiple.json | 35 + .../examples/fixtures/jsonToBru-response.bru | 35 + .../examples/fixtures/jsonToBru-response.json | 35 + .../examples/fixtures/jsonToBru-simple.bru | 30 + .../examples/fixtures/jsonToBru-simple.json | 32 + .../bruno-lang/v2/tests/fixtures/request.json | 23 +- .../bruno-schema/src/collections/index.js | 39 +- .../bruno-tests/collection/echo/echo json.bru | 1 + tests/response-examples/collection/bruno.json | 5 + .../collection/echo-request.bru | 22 + .../response-examples/create-example.spec.ts | 96 ++ tests/response-examples/edit-example.spec.ts | 119 ++ .../init-user-data/collection-security.json | 10 + .../init-user-data/preferences.json | 6 + .../response-examples/menu-operations.spec.ts | 69 ++ 122 files changed, 8693 insertions(+), 77 deletions(-) create mode 100644 packages/bruno-app/src/components/Checkbox/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/Checkbox/index.js create mode 100644 packages/bruno-app/src/components/Checkbox/index.spec.js create mode 100644 packages/bruno-app/src/components/Icons/examples/index.js create mode 100644 packages/bruno-app/src/components/RadioButton/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/RadioButton/index.js create mode 100644 packages/bruno-app/src/components/RadioButton/index.spec.js create mode 100644 packages/bruno-app/src/components/ResponseExample/CreateExampleModal/index.js create mode 100644 packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleBody/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleBody/index.js create mode 100644 packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleBodyMode/index.js create mode 100644 packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleBodyRenderer/index.js create mode 100644 packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleDescription/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleDescription/index.js create mode 100644 packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleFileBody/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleFileBody/index.js create mode 100644 packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleFormUrlEncodedParams/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleFormUrlEncodedParams/index.js create mode 100644 packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleHeaders/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleHeaders/index.js create mode 100644 packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleMultipartFormParams/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleMultipartFormParams/index.js create mode 100644 packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleParams/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleParams/index.js create mode 100644 packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleUrlBar/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleUrlBar/index.js create mode 100644 packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/index.js create mode 100644 packages/bruno-app/src/components/ResponseExample/ResponseExampleResponsePane/ResponseExampleResponseContent/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/ResponseExample/ResponseExampleResponsePane/ResponseExampleResponseContent/index.js create mode 100644 packages/bruno-app/src/components/ResponseExample/ResponseExampleResponsePane/ResponseExampleResponseHeaders/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/ResponseExample/ResponseExampleResponsePane/ResponseExampleResponseHeaders/index.js create mode 100644 packages/bruno-app/src/components/ResponseExample/ResponseExampleResponsePane/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/ResponseExample/ResponseExampleResponsePane/index.js create mode 100644 packages/bruno-app/src/components/ResponseExample/ResponseExampleTopBar/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/ResponseExample/ResponseExampleTopBar/index.js create mode 100644 packages/bruno-app/src/components/ResponseExample/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/ResponseExample/index.js create mode 100644 packages/bruno-app/src/components/ResponsePane/ResponseBookmark/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/ResponsePane/ResponseBookmark/index.js create mode 100644 packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/ExampleItem/DeleteExampleModal.js create mode 100644 packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/ExampleItem/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/ExampleItem/index.js create mode 100644 packages/bruno-app/src/components/Table-v2/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/Table-v2/index.js create mode 100644 packages/bruno-app/src/components/TruncatedText/index.js create mode 100644 packages/bruno-app/src/providers/ReduxStore/slices/collections/exampleReducers.js create mode 100644 packages/bruno-lang/v2/tests/examples/examples.spec.js create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bruToJson-basic-auth.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bruToJson-basic-auth.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bruToJson-bearer-auth.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bruToJson-bearer-auth.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bruToJson-empty-example.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bruToJson-empty-example.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bruToJson-json-body.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bruToJson-json-body.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bruToJson-multiple-examples.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bruToJson-multiple-examples.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bruToJson-no-examples.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bruToJson-no-examples.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bruToJson-response-example.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bruToJson-response-example.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bruToJson-single-example.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bruToJson-single-example.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bruToJson-text-body.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bruToJson-text-body.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bruToJson-xml-body.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bruToJson-xml-body.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/examples-complex.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/examples-simple.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/examples-simple.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/jsonToBru-auth.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/jsonToBru-auth.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/jsonToBru-bodytypes.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/jsonToBru-bodytypes.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/jsonToBru-multiple.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/jsonToBru-multiple.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/jsonToBru-response.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/jsonToBru-response.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/jsonToBru-simple.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/jsonToBru-simple.json create mode 100644 tests/response-examples/collection/bruno.json create mode 100644 tests/response-examples/collection/echo-request.bru create mode 100644 tests/response-examples/create-example.spec.ts create mode 100644 tests/response-examples/edit-example.spec.ts create mode 100644 tests/response-examples/init-user-data/collection-security.json create mode 100644 tests/response-examples/init-user-data/preferences.json create mode 100644 tests/response-examples/menu-operations.spec.ts diff --git a/packages/bruno-app/src/components/Checkbox/StyledWrapper.js b/packages/bruno-app/src/components/Checkbox/StyledWrapper.js new file mode 100644 index 0000000000..ddfffe2250 --- /dev/null +++ b/packages/bruno-app/src/components/Checkbox/StyledWrapper.js @@ -0,0 +1,79 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + .checkbox-container { + width: 1rem; + height: 1rem; + display: flex; + justify-content: center; + align-items: center; + position: relative; + cursor: pointer; + + &:disabled { + cursor: not-allowed; + opacity: 0.5; + } + } + + .checkbox-checkmark { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + visibility: ${(props) => props.checked ? 'visible' : 'hidden'}; + pointer-events: none; + } + + .checkbox-input { + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + width: 1rem; + height: 1rem; + border: 2px solid ${(props) => { + if (props.checked && props.disabled) { + return props.theme.colors.text.muted; + } + + if (props.checked && !props.disabled) { + return props.theme.colors.text.yellow; + } + + return props.theme.colors.text.muted; + }}; + border-radius: 4px; + background-color: ${(props) => { + if (props.checked && !props.disabled) { + return props.theme.colors.text.yellow; + } + + if (props.checked && props.disabled) { + return props.theme.colors.text.muted; + } + + return 'transparent'; + }}; + cursor: pointer; + position: relative; + transition: all 0.2s ease; + outline: none; + box-shadow: none; + + &:hover:not(:disabled) { + opacity: 0.8; + } + + &:disabled { + cursor: not-allowed; + opacity: 0.5; + } + + &:focus { + outline: none; + box-shadow: 0 0 0 2px ${(props) => props.theme.colors.text.yellow}40; + } + } +`; + +export default StyledWrapper; diff --git a/packages/bruno-app/src/components/Checkbox/index.js b/packages/bruno-app/src/components/Checkbox/index.js new file mode 100644 index 0000000000..2e413fb06a --- /dev/null +++ b/packages/bruno-app/src/components/Checkbox/index.js @@ -0,0 +1,45 @@ +import React from 'react'; +import StyledWrapper from './StyledWrapper'; +import { IconCheckMark } from 'components/Icons/examples'; +import { useTheme } from 'providers/Theme'; + +const Checkbox = ({ + checked = false, + disabled = false, + onChange, + className = '', + id, + name, + value, + ...props +}) => { + const { theme } = useTheme(); + + const handleChange = (e) => { + if (!disabled && onChange) { + onChange(e); + } + }; + + return ( + +
+ + +
+ +
+ ); +}; + +export default Checkbox; diff --git a/packages/bruno-app/src/components/Checkbox/index.spec.js b/packages/bruno-app/src/components/Checkbox/index.spec.js new file mode 100644 index 0000000000..f7f2a91e5f --- /dev/null +++ b/packages/bruno-app/src/components/Checkbox/index.spec.js @@ -0,0 +1,226 @@ +import '@testing-library/jest-dom'; +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { ThemeProvider } from 'styled-components'; +import Checkbox from './index'; +import themes from 'themes/index'; + +// Mock the IconCheckMark component +jest.mock('components/Icons/examples', () => ({ + IconCheckMark: ({ className, color, size }) => ( +
+ ) +})); + +// Mock the useTheme hook +jest.mock('providers/Theme', () => ({ + useTheme: () => ({ + theme: { + examples: { + checkbox: { + color: '#f59e0b' + } + } + } + }) +})); + +const renderWithTheme = (component) => { + return render({component}); +}; + +describe('Checkbox', () => { + const mockOnChange = jest.fn(); + + beforeEach(() => { + mockOnChange.mockClear(); + }); + + describe('Rendering', () => { + it('should render with default props', () => { + renderWithTheme(); + + const checkbox = screen.getByRole('checkbox'); + expect(checkbox).toBeInTheDocument(); + expect(checkbox).not.toBeChecked(); + expect(checkbox).not.toBeDisabled(); + }); + + it('should render with custom props', () => { + renderWithTheme(); + + const checkbox = screen.getByRole('checkbox'); + expect(checkbox).toHaveAttribute('id', 'test-checkbox'); + expect(checkbox).toHaveAttribute('name', 'test-name'); + expect(checkbox).toHaveAttribute('value', 'test-value'); + }); + + it('should render as checked when checked prop is true', () => { + renderWithTheme(); + + const checkbox = screen.getByRole('checkbox'); + expect(checkbox).toBeChecked(); + }); + + it('should render as disabled when disabled prop is true', () => { + renderWithTheme(); + + const checkbox = screen.getByRole('checkbox'); + expect(checkbox).toBeDisabled(); + }); + + it('should render checkmark icon', () => { + renderWithTheme(); + + const checkmark = screen.getByTestId('icon-checkmark'); + expect(checkmark).toBeInTheDocument(); + expect(checkmark).toHaveClass('checkbox-checkmark'); + }); + + it('should show checkmark when checked', () => { + renderWithTheme(); + + const checkmark = screen.getByTestId('icon-checkmark'); + expect(checkmark).toHaveStyle('visibility: visible'); + }); + + it('should hide checkmark when not checked', () => { + renderWithTheme(); + + const checkmark = screen.getByTestId('icon-checkmark'); + expect(checkmark).toHaveStyle('visibility: hidden'); + }); + }); + + describe('User Interactions', () => { + it('should call onChange when clicked and not disabled', () => { + renderWithTheme(); + + const checkbox = screen.getByRole('checkbox'); + fireEvent.click(checkbox); + + expect(mockOnChange).toHaveBeenCalledTimes(1); + }); + + it('should not call onChange when clicked and disabled', () => { + renderWithTheme(); + + const checkbox = screen.getByRole('checkbox'); + fireEvent.click(checkbox); + + expect(mockOnChange).not.toHaveBeenCalled(); + }); + + it('should not call onChange when no onChange prop is provided', () => { + renderWithTheme(); + + const checkbox = screen.getByRole('checkbox'); + fireEvent.click(checkbox); + + // Should not throw error + expect(checkbox).toBeInTheDocument(); + }); + + it('should toggle checked state when clicked', () => { + const TestWrapper = () => { + const [checked, setChecked] = React.useState(false); + + return ( + setChecked(!checked)} + /> + ); + }; + + renderWithTheme(); + + const checkbox = screen.getByRole('checkbox'); + expect(checkbox).not.toBeChecked(); + + fireEvent.click(checkbox); + expect(checkbox).toBeChecked(); + + fireEvent.click(checkbox); + expect(checkbox).not.toBeChecked(); + }); + }); + + describe('Accessibility', () => { + it('should be focusable when not disabled', () => { + renderWithTheme(); + + const checkbox = screen.getByRole('checkbox'); + checkbox.focus(); + expect(checkbox).toHaveFocus(); + }); + + it('should not be focusable when disabled', () => { + renderWithTheme(); + + const checkbox = screen.getByRole('checkbox'); + checkbox.focus(); + expect(checkbox).not.toHaveFocus(); + }); + + it('should have proper ARIA attributes', () => { + renderWithTheme(); + + const checkbox = screen.getByRole('checkbox'); + expect(checkbox).toHaveAttribute('id', 'test-checkbox'); + expect(checkbox).toHaveAttribute('name', 'test-name'); + expect(checkbox).toHaveAttribute('value', 'test-value'); + }); + }); + + describe('Styling', () => { + it('should apply custom className', () => { + renderWithTheme(); + + const wrapper = screen.getByRole('checkbox').closest('.custom-class'); + expect(wrapper).toBeInTheDocument(); + }); + + it('should pass through additional props', () => { + renderWithTheme(); + + const checkbox = screen.getByTestId('custom-checkbox'); + expect(checkbox).toBeInTheDocument(); + expect(checkbox).toHaveAttribute('aria-label', 'Custom checkbox'); + }); + }); + + describe('Edge Cases', () => { + it('should handle undefined checked prop', () => { + renderWithTheme(); + + const checkbox = screen.getByRole('checkbox'); + expect(checkbox).not.toBeChecked(); + }); + + it('should handle null checked prop', () => { + renderWithTheme(); + + const checkbox = screen.getByRole('checkbox'); + expect(checkbox).not.toBeChecked(); + }); + + it('should handle string checked prop', () => { + renderWithTheme(); + + const checkbox = screen.getByRole('checkbox'); + expect(checkbox).toBeChecked(); + }); + + it('should handle empty string className', () => { + renderWithTheme(); + + const checkbox = screen.getByRole('checkbox'); + expect(checkbox).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/bruno-app/src/components/CodeEditor/index.js b/packages/bruno-app/src/components/CodeEditor/index.js index bf6f72211a..f3f1211dd1 100644 --- a/packages/bruno-app/src/components/CodeEditor/index.js +++ b/packages/bruno-app/src/components/CodeEditor/index.js @@ -245,6 +245,10 @@ export default class CodeEditor extends React.Component { this.editor.setOption('mode', this.props.mode); } + if (this.props.readOnly !== prevProps.readOnly && this.editor) { + this.editor.setOption('readOnly', this.props.readOnly); + } + this.ignoreChangeEvent = false; } diff --git a/packages/bruno-app/src/components/FilePickerEditor/index.js b/packages/bruno-app/src/components/FilePickerEditor/index.js index 26969dde39..04f9196f52 100644 --- a/packages/bruno-app/src/components/FilePickerEditor/index.js +++ b/packages/bruno-app/src/components/FilePickerEditor/index.js @@ -5,7 +5,7 @@ import { browseFiles } from 'providers/ReduxStore/slices/collections/actions'; import { IconX } from '@tabler/icons'; import { isWindowsOS } from 'utils/common/platform'; -const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = false }) => { +const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = false, editMode = true }) => { const dispatch = useDispatch(); const filenames = (isSingleFilePicker ? [value] : value || []) .filter((v) => v != null && v != '') @@ -50,20 +50,24 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa return filenames.length + ' file(s) selected'; }; + const buttonClass = `btn btn-secondary px-1 ${editMode ? 'edit-mode' : 'view-mode'}`; + return filenames.length > 0 ? (
- -   + {editMode && ( + + )} + {editMode && <> } {renderButtonText(filenames)}
) : ( - ); diff --git a/packages/bruno-app/src/components/Icons/examples/index.js b/packages/bruno-app/src/components/Icons/examples/index.js new file mode 100644 index 0000000000..cb1fe72c65 --- /dev/null +++ b/packages/bruno-app/src/components/Icons/examples/index.js @@ -0,0 +1,59 @@ +import React from 'react'; + +export const IconEdit = ({ color = '#F39D0E', size = 16, ...props }) => { + return ( + + + + + + + + + + + + ); +}; + +export const IconCaretDown = ({ color = '#8C8C8C', ...props }) => { + return ( + + + + + + + + + + + ); +}; + +export const IconCheckMark = ({ color = '#cccccc', size = 16, ...props }) => { + return ( + + + + + ); +}; + +export const ExampleIcon = ({ color = 'white', size = 16, ...props }) => { + return ( + + + + + + + + + + + + + + ); +}; diff --git a/packages/bruno-app/src/components/Modal/index.js b/packages/bruno-app/src/components/Modal/index.js index 19d7557879..f520866a5c 100644 --- a/packages/bruno-app/src/components/Modal/index.js +++ b/packages/bruno-app/src/components/Modal/index.js @@ -26,7 +26,8 @@ const ModalFooter = ({ handleCancel, confirmDisabled, hideCancel, - hideFooter + hideFooter, + confirmButtonClass }) => { confirmText = confirmText || 'Save'; cancelText = cancelText || 'Cancel'; @@ -45,7 +46,7 @@ const ModalFooter = ({
diff --git a/packages/bruno-app/src/components/MultiLineEditor/index.js b/packages/bruno-app/src/components/MultiLineEditor/index.js index 86b4b728f1..b7bc2047be 100644 --- a/packages/bruno-app/src/components/MultiLineEditor/index.js +++ b/packages/bruno-app/src/components/MultiLineEditor/index.js @@ -18,6 +18,7 @@ class MultiLineEditor extends Component { this.cachedValue = props.value || ''; this.editorRef = React.createRef(); this.variables = {}; + this.readOnly = props.readOnly || false; this.state = { maskInput: props.isSecret || false // Always mask the input by default (if it's a secret) @@ -37,6 +38,7 @@ class MultiLineEditor extends Component { }, readOnly: this.props.readOnly ? 'nocursor' : false, tabindex: 0, + readOnly: this.readOnly, extraKeys: { 'Ctrl-Enter': () => { if (this.props.onRun) { @@ -143,6 +145,9 @@ class MultiLineEditor extends Component { // also set the maskInput flag to the new value this.setState({ maskInput: this.props.isSecret }); } + if (this.props.readOnly !== prevProps.readOnly && this.editor) { + this.editor.setOption('readOnly', this.props.readOnly || false); + } this.ignoreChangeEvent = false; } diff --git a/packages/bruno-app/src/components/RadioButton/StyledWrapper.js b/packages/bruno-app/src/components/RadioButton/StyledWrapper.js new file mode 100644 index 0000000000..9ef04cf39d --- /dev/null +++ b/packages/bruno-app/src/components/RadioButton/StyledWrapper.js @@ -0,0 +1,74 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + .radio-container { + display: flex; + justify-content: center; + align-items: center; + position: relative; + } + + .radio-input { + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + width: 16px; + height: 16px; + border: 2px solid ${(props) => props.theme.colors.text.muted}; + border-radius: 50%; + background-color: transparent; + cursor: pointer; + position: relative; + outline: none; + box-shadow: none; + margin: 0; + + &:checked { + border-color: ${(props) => props.theme.colors.text.yellow}; + background-color: transparent; + + &::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + background-color: ${(props) => props.theme.colors.text.yellow}; + } + } + + &:disabled { + cursor: not-allowed; + opacity: 0.5; + border-color: ${(props) => props.theme.colors.text.muted}; + background-color: transparent; + + &:checked { + border-color: ${(props) => props.theme.colors.text.muted}; + + &::after { + background-color: ${(props) => props.theme.colors.text.muted}; + } + } + } + + &:hover:not(:disabled) { + opacity: 0.8; + } + } + + .radio-label { + position: absolute; + top: 0; + left: 0; + width: 16px; + height: 16px; + cursor: pointer; + pointer-events: none; + } +`; + +export default StyledWrapper; diff --git a/packages/bruno-app/src/components/RadioButton/index.js b/packages/bruno-app/src/components/RadioButton/index.js new file mode 100644 index 0000000000..5a3d3a0dd7 --- /dev/null +++ b/packages/bruno-app/src/components/RadioButton/index.js @@ -0,0 +1,40 @@ +import React from 'react'; +import StyledWrapper from './StyledWrapper'; + +const RadioButton = ({ + checked, + disabled = false, + onChange, + name, + value, + id, + className = '', + ...props +}) => { + const handleChange = (e) => { + if (!disabled && onChange) { + onChange(e); + } + }; + + return ( + +
+ +
+
+ ); +}; + +export default RadioButton; diff --git a/packages/bruno-app/src/components/RadioButton/index.spec.js b/packages/bruno-app/src/components/RadioButton/index.spec.js new file mode 100644 index 0000000000..7439752715 --- /dev/null +++ b/packages/bruno-app/src/components/RadioButton/index.spec.js @@ -0,0 +1,347 @@ +import '@testing-library/jest-dom'; +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { ThemeProvider } from 'styled-components'; +import RadioButton from './index'; +import themes from 'themes/index'; + +const renderWithTheme = (component) => { + return render({component}); +}; + +describe('RadioButton', () => { + const mockOnChange = jest.fn(); + + beforeEach(() => { + mockOnChange.mockClear(); + }); + + describe('Rendering', () => { + it('should render with default props', () => { + renderWithTheme(); + + const radio = screen.getByRole('radio'); + expect(radio).toBeInTheDocument(); + expect(radio).not.toBeChecked(); + expect(radio).not.toBeDisabled(); + }); + + it('should render with custom props', () => { + renderWithTheme(); + + const radio = screen.getByRole('radio'); + expect(radio).toHaveAttribute('id', 'test-radio'); + expect(radio).toHaveAttribute('name', 'test-group'); + expect(radio).toHaveAttribute('value', 'test-value'); + }); + + it('should render as checked when checked prop is true', () => { + renderWithTheme(); + + const radio = screen.getByRole('radio'); + expect(radio).toBeChecked(); + }); + + it('should render as disabled when disabled prop is true', () => { + renderWithTheme(); + + const radio = screen.getByRole('radio'); + expect(radio).toBeDisabled(); + }); + + it('should render with label element', () => { + renderWithTheme(); + + const radio = screen.getByRole('radio'); + const label = radio.parentElement.querySelector('label'); + expect(label).toBeInTheDocument(); + expect(label).toHaveAttribute('for', 'test-radio'); + }); + }); + + describe('User Interactions', () => { + it('should call onChange when clicked and not disabled', () => { + renderWithTheme(); + + const radio = screen.getByRole('radio'); + fireEvent.click(radio); + + expect(mockOnChange).toHaveBeenCalledTimes(1); + }); + + it('should not call onChange when clicked and disabled', () => { + renderWithTheme(); + + const radio = screen.getByRole('radio'); + fireEvent.click(radio); + + expect(mockOnChange).not.toHaveBeenCalled(); + }); + + it('should call onChange when label is clicked', () => { + renderWithTheme(); + + const radio = screen.getByRole('radio'); + const label = radio.parentElement.querySelector('label'); + fireEvent.click(label); + + expect(mockOnChange).toHaveBeenCalledTimes(1); + }); + + it('should not call onChange when no onChange prop is provided', () => { + renderWithTheme(); + + const radio = screen.getByRole('radio'); + fireEvent.click(radio); + + // Should not throw error + expect(radio).toBeInTheDocument(); + }); + + it('should stay checked when clicked (radio button behavior)', () => { + const TestWrapper = () => { + const [checked, setChecked] = React.useState(false); + + return ( + setChecked(true)} + /> + ); + }; + + renderWithTheme(); + + const radio = screen.getByRole('radio'); + expect(radio).not.toBeChecked(); + + fireEvent.click(radio); + expect(radio).toBeChecked(); + + // Radio buttons stay checked when clicked again + fireEvent.click(radio); + expect(radio).toBeChecked(); + }); + }); + + describe('Radio Group Behavior', () => { + it('should work correctly in a radio group', () => { + const TestWrapper = () => { + const [selectedValue, setSelectedValue] = React.useState('option1'); + + return ( +
+ setSelectedValue('option1')} + /> + setSelectedValue('option2')} + /> +
+ ); + }; + + renderWithTheme(); + + const radios = screen.getAllByRole('radio'); + const option1 = radios.find((radio) => radio.value === 'option1'); + const option2 = radios.find((radio) => radio.value === 'option2'); + + expect(option1).toBeChecked(); + expect(option2).not.toBeChecked(); + + fireEvent.click(option2); + + expect(option1).not.toBeChecked(); + expect(option2).toBeChecked(); + }); + + it('should maintain radio group behavior with multiple options', () => { + const TestWrapper = () => { + const [selectedValue, setSelectedValue] = React.useState('option1'); + + return ( +
+ setSelectedValue('option1')} + /> + setSelectedValue('option2')} + /> + setSelectedValue('option3')} + /> +
+ ); + }; + + renderWithTheme(); + + const radios = screen.getAllByRole('radio'); + const option1 = radios.find((radio) => radio.value === 'option1'); + const option2 = radios.find((radio) => radio.value === 'option2'); + const option3 = radios.find((radio) => radio.value === 'option3'); + + expect(option1).toBeChecked(); + expect(option2).not.toBeChecked(); + expect(option3).not.toBeChecked(); + + fireEvent.click(option3); + + expect(option1).not.toBeChecked(); + expect(option2).not.toBeChecked(); + expect(option3).toBeChecked(); + }); + }); + + describe('Accessibility', () => { + it('should be focusable when not disabled', () => { + renderWithTheme(); + + const radio = screen.getByRole('radio'); + radio.focus(); + expect(radio).toHaveFocus(); + }); + + it('should not be focusable when disabled', () => { + renderWithTheme(); + + const radio = screen.getByRole('radio'); + radio.focus(); + expect(radio).not.toHaveFocus(); + }); + + it('should have proper ARIA attributes', () => { + renderWithTheme(); + + const radio = screen.getByRole('radio'); + expect(radio).toHaveAttribute('id', 'test-radio'); + expect(radio).toHaveAttribute('name', 'test-group'); + expect(radio).toHaveAttribute('value', 'test-value'); + }); + + it('should have associated label', () => { + renderWithTheme(); + + const radio = screen.getByRole('radio'); + const label = radio.parentElement.querySelector('label'); + + expect(label).toHaveAttribute('for', 'test-radio'); + expect(radio).toHaveAttribute('id', 'test-radio'); + }); + }); + + describe('Styling', () => { + it('should apply custom className to container', () => { + renderWithTheme(); + + const container = screen.getByRole('radio').closest('.custom-class'); + expect(container).toBeInTheDocument(); + }); + + it('should pass through additional props', () => { + renderWithTheme(); + + const radio = screen.getByTestId('custom-radio'); + expect(radio).toBeInTheDocument(); + expect(radio).toHaveAttribute('aria-label', 'Custom radio button'); + }); + }); + + describe('Edge Cases', () => { + it('should handle undefined checked prop', () => { + renderWithTheme(); + + const radio = screen.getByRole('radio'); + expect(radio).not.toBeChecked(); + }); + + it('should handle null checked prop', () => { + renderWithTheme(); + + const radio = screen.getByRole('radio'); + expect(radio).not.toBeChecked(); + }); + + it('should handle string checked prop', () => { + renderWithTheme(); + + const radio = screen.getByRole('radio'); + expect(radio).toBeChecked(); + }); + + it('should handle empty string className', () => { + renderWithTheme(); + + const radio = screen.getByRole('radio'); + expect(radio).toBeInTheDocument(); + }); + + it('should handle missing id prop', () => { + renderWithTheme(); + + const radio = screen.getByRole('radio'); + expect(radio).toBeInTheDocument(); + expect(radio).not.toHaveAttribute('id'); + }); + + it('should handle missing name prop', () => { + renderWithTheme(); + + const radio = screen.getByRole('radio'); + expect(radio).toBeInTheDocument(); + expect(radio).not.toHaveAttribute('name'); + }); + + it('should handle missing value prop', () => { + renderWithTheme(); + + const radio = screen.getByRole('radio'); + expect(radio).toBeInTheDocument(); + expect(radio).not.toHaveAttribute('value'); + }); + }); + + describe('Keyboard Navigation', () => { + it('should be focusable and respond to click after focus', () => { + renderWithTheme(); + + const radio = screen.getByRole('radio'); + radio.focus(); + expect(radio).toHaveFocus(); + + // Radio buttons don't trigger onChange on keyDown, but can be clicked after focus + fireEvent.click(radio); + expect(mockOnChange).toHaveBeenCalledTimes(1); + }); + + it('should not be focusable when disabled', () => { + renderWithTheme(); + + const radio = screen.getByRole('radio'); + radio.focus(); + + expect(radio).not.toHaveFocus(); + }); + }); +}); diff --git a/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js b/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js index c5b1362879..4759039622 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js +++ b/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js @@ -8,6 +8,7 @@ import { useTheme } from 'providers/Theme'; import { IconDeviceFloppy, IconArrowRight, IconCode } from '@tabler/icons'; import SingleLineEditor from 'components/SingleLineEditor'; import { isMacOS } from 'utils/common/platform'; +import { hasRequestChanges } from 'utils/collections'; import StyledWrapper from './StyledWrapper'; import GenerateCodeItem from 'components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index'; import toast from 'react-hot-toast'; @@ -133,15 +134,15 @@ const QueryUrl = ({ item, collection, handleRun }) => { className="infotip mr-3" onClick={(e) => { e.stopPropagation(); - if (!item.draft) return; + if (!hasRequestChanges(item)) return; onSave(); }} > Save ({saveShortcut}) diff --git a/packages/bruno-app/src/components/RequestPane/WsQueryUrl/index.js b/packages/bruno-app/src/components/RequestPane/WsQueryUrl/index.js index 0c5f65d7d2..7232d2ec43 100644 --- a/packages/bruno-app/src/components/RequestPane/WsQueryUrl/index.js +++ b/packages/bruno-app/src/components/RequestPane/WsQueryUrl/index.js @@ -10,6 +10,7 @@ import toast from 'react-hot-toast'; import { useDispatch } from 'react-redux'; import { getPropertyFromDraftOrRequest } from 'utils/collections'; import { isMacOS } from 'utils/common/platform'; +import { hasRequestChanges } from 'utils/collections'; import { closeWsConnection, isWsConnectionActive } from 'utils/network/index'; import StyledWrapper from './StyledWrapper'; import get from 'lodash/get'; @@ -108,15 +109,15 @@ const WsQueryUrl = ({ item, collection, handleRun }) => { className="infotip mr-3" onClick={(e) => { e.stopPropagation(); - if (!item.draft) return; + if (!hasRequestChanges(item)) return; onSave(); }} > Save ({saveShortcut}) diff --git a/packages/bruno-app/src/components/RequestTabPanel/index.js b/packages/bruno-app/src/components/RequestTabPanel/index.js index ec3878a7fc..11671ece49 100644 --- a/packages/bruno-app/src/components/RequestTabPanel/index.js +++ b/packages/bruno-app/src/components/RequestTabPanel/index.js @@ -32,6 +32,7 @@ import FolderNotFound from './FolderNotFound'; import WsQueryUrl from 'components/RequestPane/WsQueryUrl'; import WSRequestPane from 'components/RequestPane/WSRequestPane'; import WSResponsePane from 'components/ResponsePane/WsResponsePane'; +import ResponseExample from 'components/ResponseExample'; const MIN_LEFT_PANE_WIDTH = 300; const MIN_RIGHT_PANE_WIDTH = 350; @@ -186,6 +187,32 @@ const RequestTabPanel = () => { return
Collection not found!
; } + if (focusedTab.type === 'response-example') { + const item = findItemInCollection(collection, focusedTab.itemUid); + const example = item?.examples?.find((ex) => ex.uid === focusedTab.uid); + + if (!item) { + return ( +
+ Item not found! ItemUid: + {focusedTab.itemUid} +
+ ); + } + + if (!example) { + return ( +
+ Example not found! Item: + {item?.uid} + , ExampleUid: + {focusedTab.uid} +
+ ); + } + return ; + } + const item = findItemInCollection(collection, activeTabUid); const isGrpcRequest = item?.type === 'grpc-request'; const isWsRequest = item?.type === 'ws-request'; diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/ConfirmRequestClose/index.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/ConfirmRequestClose/index.js index d02704636a..293ebb3a6f 100644 --- a/packages/bruno-app/src/components/RequestTabs/RequestTab/ConfirmRequestClose/index.js +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/ConfirmRequestClose/index.js @@ -2,7 +2,11 @@ import React from 'react'; import { IconAlertTriangle } from '@tabler/icons'; import Modal from 'components/Modal'; -const ConfirmRequestClose = ({ item, onCancel, onCloseWithoutSave, onSaveAndClose }) => { +const ConfirmRequestClose = ({ item, example, onCancel, onCloseWithoutSave, onSaveAndClose }) => { + const isExample = !!example; + const itemName = isExample ? example.name : item.name; + const itemType = isExample ? 'example' : 'request'; + return ( Hold on..
- You have unsaved changes in request {item.name}. + You have unsaved changes in {itemType} {itemName}.
diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js index 71d57d940c..0fac7e0a24 100644 --- a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js @@ -7,7 +7,8 @@ import { useTheme } from 'providers/Theme'; import { useDispatch } from 'react-redux'; import darkTheme from 'themes/dark'; import lightTheme from 'themes/light'; -import { findItemInCollection } from 'utils/collections'; +import { findItemInCollection, hasRequestChanges, hasExampleChanges } from 'utils/collections'; +import { ExampleIcon } from 'components/Icons/examples'; import ConfirmRequestClose from './ConfirmRequestClose'; import RequestTabNotFound from './RequestTabNotFound'; import SpecialTab from './SpecialTab'; @@ -29,6 +30,14 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi const dropdownTippyRef = useRef(); const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); + // Get item and example data for both request and example tabs + const item = tab.type === 'response-example' + ? findItemInCollection(collection, tab.itemUid) + : findItemInCollection(collection, tab.uid); + const example = tab.type === 'response-example' + ? item?.examples?.find((ex) => ex.uid === tab.uid) + : null; + const handleCloseClick = (event) => { event.stopPropagation(); event.preventDefault(); @@ -92,7 +101,71 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi ); } - const item = findItemInCollection(collection, tab.uid); + // Handle response-example tabs specially + if (tab.type === 'response-example') { + const hasChanges = hasExampleChanges(item, tab.uid); + + if (!item || !example) { + return ( + { + if (e.button === 1) { + e.preventDefault(); + e.stopPropagation(); + + dispatch(closeTabs({ tabUids: [tab.uid] })); + } + }} + > + + + ); + } + + return ( + +
dispatch(makeTabPermanent({ uid: tab.uid }))} + onMouseUp={(e) => { + if (!hasChanges) return handleMouseUp(e); + + if (e.button === 1) { + e.stopPropagation(); + e.preventDefault(); + setShowConfirmClose(true); + } + }} + > + + + {example.name} + +
+
{ + if (!hasChanges) { + return handleCloseClick(e); + } + + e.stopPropagation(); + e.preventDefault(); + setShowConfirmClose(true); + }} + > + {!hasChanges ? ( + + ) : ( + + )} +
+
+ ); + } + const getMethodText = useCallback((item) => { if (!item) return; @@ -135,6 +208,7 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi {showConfirmClose && ( setShowConfirmClose(false)} onCloseWithoutSave={() => { isWS && closeWsConnection(item.uid); @@ -172,7 +246,7 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi onContextMenu={handleRightClick} onDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} onMouseUp={(e) => { - if (!item.draft) return handleMouseUp(e); + if (!hasRequestChanges(item)) return handleMouseUp(e); if (e.button === 1) { e.stopPropagation(); @@ -200,7 +274,7 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
{ - if (!item.draft) { + if (!hasRequestChanges(item)) { isWS && closeWsConnection(item.uid); return handleCloseClick(e); }; @@ -210,7 +284,7 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi setShowConfirmClose(true); }} > - {!item.draft ? ( + {!hasRequestChanges(item) ? ( ) : ( @@ -243,7 +317,7 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col try { const item = findItemInCollection(collection, tabUid); // silently save unsaved changes before closing the tab - if (item.draft) { + if (hasRequestChanges(item)) { await dispatch(saveRequest(item.uid, collection.uid, true)); } @@ -295,7 +369,7 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col event.stopPropagation(); const items = flattenItems(collection?.items); - const savedTabs = items?.filter?.((item) => !item.draft); + const savedTabs = items?.filter?.((item) => !hasRequestChanges(item)); const savedTabIds = savedTabs?.map((item) => item.uid) || []; dispatch(closeTabs({ tabUids: savedTabIds })); } diff --git a/packages/bruno-app/src/components/ResponseExample/CreateExampleModal/index.js b/packages/bruno-app/src/components/ResponseExample/CreateExampleModal/index.js new file mode 100644 index 0000000000..7066676bae --- /dev/null +++ b/packages/bruno-app/src/components/ResponseExample/CreateExampleModal/index.js @@ -0,0 +1,84 @@ +import React, { useState } from 'react'; +import Modal from 'components/Modal'; + +const CreateExampleModal = ({ isOpen, onClose, onSave, title = 'Create Response Example' }) => { + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + + const handleConfirm = () => { + if (name.trim()) { + onSave(name.trim(), description.trim()); + // Reset form + setName(''); + setDescription(''); + } + }; + + const handleClose = () => { + // Reset form when closing + setName(''); + setDescription(''); + onClose(); + }; + + // Reset form when modal opens + React.useEffect(() => { + if (isOpen) { + setName(''); + setDescription(''); + } + }, [isOpen]); + + if (!isOpen) { + return null; + } + + return ( + +
+
+ + setName(e.target.value)} + placeholder="Enter example name..." + autoFocus + required + data-testid="create-example-name-input" + /> +
+ +
+ +