From 68cbb7d9df9aac678805e9533907a5dc0325017e Mon Sep 17 00:00:00 2001 From: sanish chirayath Date: Sat, 1 Nov 2025 05:56:11 +0530 Subject: [PATCH] Feat/add import export support for examples (#5936) * 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 fix: response header disabled by default issue Feat/with bru example parser (#5892) * fix: response header disabled by default issue feat: new parsing logic fix: change test cases to accomodate new brulang add: path params features rm:consoles six: make tab permanent on double click fix width feat: add status editing feat: review fixes review fixes fix: review fixes fix: post review mv: test files fix: review * fix: lint * fix: review comments * fix: icons folder strcuture fix: tests fix: lint fix: unit tests feat: body mode selector fix: close all collections rm: example feat added tests. lang change feat: add custom status text fix: status update feat: add body mode, update tests add default name prefilled for example fix: active tab styles, prefilled name, text fixes fix : pkg lock fix: review fix: review comments fix: hide cursor when readonly fix: height fix: null body fix: response body parsing fix: test cases feat: add method support for examples fix: reponse parsing fix: update response body type when content type is updated rm : commented code feat: update parser logic fix: organize files feat: enhance examples handling in collection export and import feat: postman imports fro examples feat: enhance OpenAPI import functionality to support examples feat: support postman export fix: postman export import fix: open api tests, remove requestbody related logic rm: examples fix: move common attributes files ui fixes fix: clone issue fix: create example from request menu review fixes more review fixes mv: files, fix mode req error organize files fix:tests fix: save dot issue fix: bugs fix: postman export fix: import path params * chore:improve modal handling in environment and response example tests fix: test issues resolved * chore: update response example tests to use new fixture files and improve cleanup logic --------- Co-authored-by: Abhishek S Lal Co-authored-by: Bijin Bruno --- .../src/components/BodyModeSelector/index.js | 83 ++ .../src/components/Checkbox/StyledWrapper.js | 79 ++ .../src/components/Checkbox/index.js | 45 + .../components/CodeEditor/StyledWrapper.js | 12 + .../src/components/CodeEditor/index.js | 8 +- .../src/components/FilePickerEditor/index.js | 18 +- .../src/components/Icons/ExampleIcon/index.js | 21 + .../components/Icons/IconCaretDown/index.js | 18 + .../components/Icons/IconCheckMark/index.js | 11 + .../src/components/Icons/IconEdit/index.js | 19 + .../bruno-app/src/components/Modal/index.js | 9 +- .../src/components/MultiLineEditor/index.js | 4 + .../components/RadioButton/StyledWrapper.js | 74 + .../src/components/RadioButton/index.js | 40 + .../components/RequestPane/QueryUrl/index.js | 10 +- .../RequestPane/WsQueryUrl/index.js | 10 +- .../RequestTabPanel/ExampleNotFound/index.js | 40 + .../src/components/RequestTabPanel/index.js | 12 + .../RequestTabs/ExampleTab/index.js | 146 ++ .../RequestTab/ConfirmRequestClose/index.js | 8 +- .../RequestTabs/RequestTab/index.js | 33 +- .../CreateExampleModal/index.js | 102 ++ .../ResponseExampleBody/StyledWrapper.js | 80 ++ .../ResponseExampleBody/index.js | 80 ++ .../ResponseExampleBodyMode/index.js | 98 ++ .../ResponseExampleBodyRenderer/index.js | 104 ++ .../StyledWrapper.js | 38 + .../ResponseExampleDescription/index.js | 48 + .../ResponseExampleFileBody/StyledWrapper.js | 131 ++ .../ResponseExampleFileBody/index.js | 222 +++ .../StyledWrapper.js | 80 ++ .../index.js | 180 +++ .../ResponseExampleHeaders/StyledWrapper.js | 60 + .../ResponseExampleHeaders/index.js | 213 +++ .../StyledWrapper.js | 100 ++ .../index.js | 263 ++++ .../ResponseExampleParams/StyledWrapper.js | 89 ++ .../ResponseExampleParams/index.js | 272 ++++ .../ResponseExampleUrlBar/StyledWrapper.js | 53 + .../ResponseExampleUrlBar/index.js | 81 ++ .../StyledWrapper.js | 32 + .../ResponseExampleRequestPane/index.js | 47 + .../StyledWrapper.js | 16 + .../ResponseExampleResponseContent/index.js | 94 ++ .../StyledWrapper.js | 56 + .../ResponseExampleResponseHeaders/index.js | 240 ++++ .../StyledWrapper.js | 83 ++ .../ResponseExampleStatusInput/index.js | 208 +++ .../StyledWrapper.js | 39 + .../ResponseExampleResponsePane/index.js | 102 ++ .../ResponseExampleTopBar/StyledWrapper.js | 84 ++ .../ResponseExampleTopBar/index.js | 204 +++ .../ResponseExample/StyledWrapper.js | 67 + .../src/components/ResponseExample/index.js | 224 +++ .../ResponseBookmark/StyledWrapper.js | 8 + .../ResponsePane/ResponseBookmark/index.js | 118 ++ .../ResponsePane/StatusCode/index.js | 4 +- .../src/components/ResponsePane/index.js | 2 + .../DeleteResponseExampleModal/index.js | 37 + .../ExampleItem/StyledWrapper.js | 57 + .../CollectionItem/ExampleItem/index.js | 235 ++++ .../CollectionItem/GenerateCodeItem/index.js | 71 +- .../Collection/CollectionItem/index.js | 111 +- .../SingleLineEditor/StyledWrapper.js | 12 + .../src/components/SingleLineEditor/index.js | 7 +- .../src/components/Table-v2/StyledWrapper.js | 77 + .../src/components/Table-v2/index.js | 109 ++ .../src/components/TruncatedText/index.js | 107 ++ packages/bruno-app/src/globalStyles.js | 36 + .../App/ConfirmAppClose/SaveRequestsModal.js | 37 +- .../slices/collections/exampleReducers.js | 1249 +++++++++++++++++ .../ReduxStore/slices/collections/index.js | 84 +- .../src/providers/ReduxStore/slices/tabs.js | 11 +- 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/export.js | 23 + .../bruno-app/src/utils/collections/index.js | 139 +- .../bruno-app/src/utils/importers/common.js | 51 + .../src/utils/responseBodyProcessor.js | 23 + .../examples-export-import.spec.js | 584 ++++++++ packages/bruno-cli/src/utils/bru.js | 3 +- .../src/openapi/openapi-to-bruno.js | 141 ++ .../src/postman/bruno-to-postman.js | 98 +- .../src/postman/postman-to-bruno.js | 175 +++ .../openapi/openapi-with-examples.spec.js | 304 ++++ .../tests/postman-with-examples.spec.js | 233 +++ .../bruno-to-postman-with-examples.spec.js | 480 +++++++ .../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 | 98 +- packages/bruno-lang/v2/src/bruToJson.js | 49 +- .../bruno-lang/v2/src/common/attributes.js | 85 ++ .../v2/src/common/semantic-utils.js | 168 +++ .../bruno-lang/v2/src/example/bruToJson.js | 130 ++ .../bruno-lang/v2/src/example/jsonToBru.js | 245 ++++ .../v2/src/example/request/bruToJson.js | 224 +++ .../v2/src/example/response/bruToJson.js | 136 ++ packages/bruno-lang/v2/src/jsonToBru.js | 12 +- packages/bruno-lang/v2/src/utils.js | 5 +- .../v2/tests/examples/examples.spec.js | 309 ++++ .../fixtures/bru/bruToJson-empty-example.bru | 8 + .../fixtures/bru/bruToJson-json-body.bru | 21 + .../bru/bruToJson-multiple-examples.bru | 35 + .../fixtures/bru/bruToJson-no-examples.bru | 8 + .../bru/bruToJson-response-example.bru | 40 + .../fixtures/bru/bruToJson-single-example.bru | 19 + .../fixtures/bru/bruToJson-text-body.bru | 19 + .../fixtures/bru/bruToJson-xml-body.bru | 22 + .../fixtures/bru/complex-with-auth.bru | 267 ++++ .../fixtures/bru/examples-complex.bru | 123 ++ .../examples/fixtures/bru/examples-simple.bru | 71 + .../fixtures/bru/form-data-complex.bru | 132 ++ .../fixtures/bru/jsonToBru-bodytypes.bru | 58 + .../fixtures/bru/jsonToBru-multiple.bru | 35 + .../fixtures/bru/jsonToBru-response.bru | 40 + .../fixtures/bru/jsonToBru-simple.bru | 24 + .../bru/multiple-examples-variations.bru | 299 ++++ .../examples/fixtures/bru/oauth2-examples.bru | 248 ++++ .../json/bruToJson-empty-example.json | 10 + .../fixtures/json/bruToJson-json-body.json | 21 + .../json/bruToJson-multiple-examples.json | 36 + .../fixtures/json/bruToJson-no-examples.json | 11 + .../json/bruToJson-response-example.json | 39 + .../json/bruToJson-single-example.json | 24 + .../fixtures/json/bruToJson-text-body.json | 21 + .../fixtures/json/bruToJson-xml-body.json | 21 + .../fixtures/json/complex-with-auth.json | 211 +++ .../fixtures/json/examples-complex.json | 83 ++ .../fixtures/json/examples-simple.json | 52 + .../fixtures/json/form-data-complex.json | 198 +++ .../fixtures/json/jsonToBru-bodytypes.json | 48 + .../fixtures/json/jsonToBru-multiple.json | 35 + .../fixtures/json/jsonToBru-response.json | 38 + .../fixtures/json/jsonToBru-simple.json | 24 + .../json/multiple-examples-variations.json | 223 +++ .../fixtures/json/oauth2-examples.json | 185 +++ .../bruno-lang/v2/tests/fixtures/request.json | 23 +- .../bruno-schema/src/collections/index.js | 39 +- .../create-requests/grpc-requests.spec.ts | 1 + .../collection-env-create.spec.ts | 5 +- .../global-env-create.spec.ts | 5 +- .../collection-env-import.spec.ts | 9 +- .../global-env-import.spec.ts | 1 + .../bruno/fixtures/bruno-with-examples.json | 96 ++ .../bruno/import-bruno-with-examples.spec.ts | 49 + .../fixtures/openapi-with-examples.yaml | 155 ++ .../import-openapi-with-examples.spec.ts | 248 ++++ .../fixtures/postman-with-examples.json | 116 ++ .../import-postman-with-examples.spec.ts | 111 ++ .../response-examples/create-example.spec.ts | 118 ++ tests/response-examples/edit-example.spec.ts | 182 +++ .../fixtures/collection/bruno.json | 5 + .../fixtures/collection/create-example.bru | 22 + .../fixtures/collection/edit-example.bru | 22 + .../fixtures/collection/menu-operations.bru | 22 + .../init-user-data/collection-security.json | 10 + .../init-user-data/preferences.json | 6 + .../response-examples/menu-operations.spec.ts | 114 ++ 160 files changed, 14663 insertions(+), 102 deletions(-) create mode 100644 packages/bruno-app/src/components/BodyModeSelector/index.js 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/Icons/ExampleIcon/index.js create mode 100644 packages/bruno-app/src/components/Icons/IconCaretDown/index.js create mode 100644 packages/bruno-app/src/components/Icons/IconCheckMark/index.js create mode 100644 packages/bruno-app/src/components/Icons/IconEdit/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/RequestTabPanel/ExampleNotFound/index.js create mode 100644 packages/bruno-app/src/components/RequestTabs/ExampleTab/index.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/ResponseExampleStatusInput/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/ResponseExample/ResponseExampleResponsePane/ResponseExampleStatusInput/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/DeleteResponseExampleModal/index.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-app/src/utils/responseBodyProcessor.js create mode 100644 packages/bruno-app/src/utils/tests/collections/examples-export-import.spec.js create mode 100644 packages/bruno-converters/tests/openapi/openapi-with-examples.spec.js create mode 100644 packages/bruno-converters/tests/postman-with-examples.spec.js create mode 100644 packages/bruno-converters/tests/postman/bruno-to-postman-with-examples.spec.js create mode 100644 packages/bruno-lang/v2/src/common/attributes.js create mode 100644 packages/bruno-lang/v2/src/common/semantic-utils.js create mode 100644 packages/bruno-lang/v2/src/example/bruToJson.js create mode 100644 packages/bruno-lang/v2/src/example/jsonToBru.js create mode 100644 packages/bruno-lang/v2/src/example/request/bruToJson.js create mode 100644 packages/bruno-lang/v2/src/example/response/bruToJson.js create mode 100644 packages/bruno-lang/v2/tests/examples/examples.spec.js create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bru/bruToJson-empty-example.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bru/bruToJson-json-body.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bru/bruToJson-multiple-examples.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bru/bruToJson-no-examples.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bru/bruToJson-response-example.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bru/bruToJson-single-example.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bru/bruToJson-text-body.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bru/bruToJson-xml-body.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bru/complex-with-auth.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bru/examples-complex.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bru/examples-simple.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bru/form-data-complex.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bru/jsonToBru-bodytypes.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bru/jsonToBru-multiple.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bru/jsonToBru-response.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bru/jsonToBru-simple.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bru/multiple-examples-variations.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/bru/oauth2-examples.bru create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/json/bruToJson-empty-example.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/json/bruToJson-json-body.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/json/bruToJson-multiple-examples.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/json/bruToJson-no-examples.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/json/bruToJson-response-example.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/json/bruToJson-single-example.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/json/bruToJson-text-body.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/json/bruToJson-xml-body.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/json/complex-with-auth.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/json/examples-complex.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/json/examples-simple.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/json/form-data-complex.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/json/jsonToBru-bodytypes.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/json/jsonToBru-multiple.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/json/jsonToBru-response.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/json/jsonToBru-simple.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/json/multiple-examples-variations.json create mode 100644 packages/bruno-lang/v2/tests/examples/fixtures/json/oauth2-examples.json create mode 100644 tests/import/bruno/fixtures/bruno-with-examples.json create mode 100644 tests/import/bruno/import-bruno-with-examples.spec.ts create mode 100644 tests/import/openapi/fixtures/openapi-with-examples.yaml create mode 100644 tests/import/openapi/import-openapi-with-examples.spec.ts create mode 100644 tests/import/postman/fixtures/postman-with-examples.json create mode 100644 tests/import/postman/import-postman-with-examples.spec.ts 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/fixtures/collection/bruno.json create mode 100644 tests/response-examples/fixtures/collection/create-example.bru create mode 100644 tests/response-examples/fixtures/collection/edit-example.bru create mode 100644 tests/response-examples/fixtures/collection/menu-operations.bru 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/BodyModeSelector/index.js b/packages/bruno-app/src/components/BodyModeSelector/index.js new file mode 100644 index 000000000..842c3f9ce --- /dev/null +++ b/packages/bruno-app/src/components/BodyModeSelector/index.js @@ -0,0 +1,83 @@ +import React, { useRef, forwardRef } from 'react'; +import { IconCaretDown } from '@tabler/icons'; +import Dropdown from 'components/Dropdown'; +import { humanizeRequestBodyMode } from 'utils/collections'; + +const DEFAULT_MODES = [ + { key: 'multipartForm', label: 'Multipart Form', category: 'Form' }, + { key: 'formUrlEncoded', label: 'Form URL Encoded', category: 'Form' }, + { key: 'json', label: 'JSON', category: 'Raw' }, + { key: 'xml', label: 'XML', category: 'Raw' }, + { key: 'text', label: 'TEXT', category: 'Raw' }, + { key: 'sparql', label: 'SPARQL', category: 'Raw' }, + { key: 'file', label: 'File / Binary', category: 'Other' }, + { key: 'none', label: 'None', category: 'Other' } +]; + +const BodyModeSelector = ({ + currentMode, + onModeChange, + modes = DEFAULT_MODES, + disabled = false, + className = '', + wrapperClassName = '', + showCategories = true, + placement = 'bottom-end' +}) => { + const dropdownTippyRef = useRef(); + const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); + + const Icon = forwardRef((props, ref) => { + return ( +
+ {humanizeRequestBodyMode(currentMode)} + {' '} + +
+ ); + }); + + const onModeSelect = (mode) => { + dropdownTippyRef.current.hide(); + onModeChange(mode); + }; + + // Group modes by category for rendering + const groupedModes = modes.reduce((acc, mode) => { + const category = mode.category || 'Other'; + if (!acc[category]) { + acc[category] = []; + } + acc[category].push(mode); + return acc; + }, {}); + + return ( +
+ } + placement={placement} + disabled={disabled} + className={className} + > + {Object.entries(groupedModes).map(([category, categoryModes]) => ( + + {showCategories &&
{category}
} + {categoryModes.map((mode) => ( +
onModeSelect(mode.key)} + > + {mode.label} +
+ ))} +
+ ))} +
+
+ ); +}; + +export default BodyModeSelector; 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 000000000..ddfffe225 --- /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 000000000..175292db0 --- /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/IconCheckMark'; +import { useTheme } from 'providers/Theme'; + +const Checkbox = ({ + checked = false, + disabled = false, + onChange, + className = '', + id, + name, + value, + dataTestId = 'checkbox' +}) => { + const { theme } = useTheme(); + + const handleChange = (e) => { + if (!disabled && onChange) { + onChange(e); + } + }; + + return ( + +
+ + +
+ +
+ ); +}; + +export default Checkbox; diff --git a/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js b/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js index bce574f3c..8d2586c8b 100644 --- a/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js +++ b/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js @@ -1,6 +1,18 @@ import styled from 'styled-components'; const StyledWrapper = styled.div` + &.read-only { + div.CodeMirror .CodeMirror-lines { + user-select: none !important; + -webkit-user-select: none !important; + -ms-user-select: none !important; + } + + div.CodeMirror .CodeMirror-cursor { + display: none !important; + } + } + div.CodeMirror { background: ${(props) => props.theme.codemirror.bg}; border: solid 1px ${(props) => props.theme.codemirror.border}; diff --git a/packages/bruno-app/src/components/CodeEditor/index.js b/packages/bruno-app/src/components/CodeEditor/index.js index bf6f72211..721f35b42 100644 --- a/packages/bruno-app/src/components/CodeEditor/index.js +++ b/packages/bruno-app/src/components/CodeEditor/index.js @@ -63,7 +63,7 @@ export default class CodeEditor extends React.Component { foldGutter: true, gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'], lint: this.lintOptions, - readOnly: this.props.readOnly, + readOnly: this.props.readOnly ? 'nocursor' : false, scrollbarStyle: 'overlay', theme: this.props.theme === 'dark' ? 'monokai' : 'default', extraKeys: { @@ -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 ? 'nocursor' : false); + } + this.ignoreChangeEvent = false; } @@ -262,7 +266,7 @@ export default class CodeEditor extends React.Component { } return ( { +const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = false, readOnly = false }) => { 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 ${readOnly ? 'view-mode' : 'edit-mode'}`; + return filenames.length > 0 ? (
- -   + {!readOnly && ( + + )} + {!readOnly && <> } {renderButtonText(filenames)}
) : ( - ); diff --git a/packages/bruno-app/src/components/Icons/ExampleIcon/index.js b/packages/bruno-app/src/components/Icons/ExampleIcon/index.js new file mode 100644 index 000000000..aa7a5c47b --- /dev/null +++ b/packages/bruno-app/src/components/Icons/ExampleIcon/index.js @@ -0,0 +1,21 @@ +import React from 'react'; + +const ExampleIcon = ({ color = 'white', size = 16, ...props }) => { + return ( + + + + + + + + + + + + + + ); +}; + +export default ExampleIcon; diff --git a/packages/bruno-app/src/components/Icons/IconCaretDown/index.js b/packages/bruno-app/src/components/Icons/IconCaretDown/index.js new file mode 100644 index 000000000..3eb4bb7cf --- /dev/null +++ b/packages/bruno-app/src/components/Icons/IconCaretDown/index.js @@ -0,0 +1,18 @@ +import React from 'react'; + +const IconCaretDown = ({ color = '#8C8C8C', ...props }) => { + return ( + + + + + + + + + + + ); +}; + +export default IconCaretDown; diff --git a/packages/bruno-app/src/components/Icons/IconCheckMark/index.js b/packages/bruno-app/src/components/Icons/IconCheckMark/index.js new file mode 100644 index 000000000..86b5bd501 --- /dev/null +++ b/packages/bruno-app/src/components/Icons/IconCheckMark/index.js @@ -0,0 +1,11 @@ +import React from 'react'; + +const IconCheckMark = ({ color = '#cccccc', size = 16, ...props }) => { + return ( + + + + ); +}; + +export default IconCheckMark; diff --git a/packages/bruno-app/src/components/Icons/IconEdit/index.js b/packages/bruno-app/src/components/Icons/IconEdit/index.js new file mode 100644 index 000000000..a02d7b646 --- /dev/null +++ b/packages/bruno-app/src/components/Icons/IconEdit/index.js @@ -0,0 +1,19 @@ +import React from 'react'; + +const IconEdit = ({ color = '#F39D0E', size = 16, ...props }) => { + return ( + + + + + + + + + + + + ); +}; + +export default IconEdit; diff --git a/packages/bruno-app/src/components/Modal/index.js b/packages/bruno-app/src/components/Modal/index.js index 19d755787..35828b30e 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 = 'btn-secondary' }) => { confirmText = confirmText || 'Save'; cancelText = cancelText || 'Cancel'; @@ -45,7 +46,7 @@ const ModalFooter = ({ + + ); +}; + +export default ExampleNotFound; diff --git a/packages/bruno-app/src/components/RequestTabPanel/index.js b/packages/bruno-app/src/components/RequestTabPanel/index.js index ec3878a7f..f7880e509 100644 --- a/packages/bruno-app/src/components/RequestTabPanel/index.js +++ b/packages/bruno-app/src/components/RequestTabPanel/index.js @@ -29,9 +29,11 @@ import CollectionOverview from 'components/CollectionSettings/Overview'; import RequestNotLoaded from './RequestNotLoaded'; import RequestIsLoading from './RequestIsLoading'; import FolderNotFound from './FolderNotFound'; +import ExampleNotFound from './ExampleNotFound'; 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 +188,16 @@ 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 (!example) { + return ; + } + 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/ExampleTab/index.js b/packages/bruno-app/src/components/RequestTabs/ExampleTab/index.js new file mode 100644 index 000000000..54af20466 --- /dev/null +++ b/packages/bruno-app/src/components/RequestTabs/ExampleTab/index.js @@ -0,0 +1,146 @@ +import React, { useState, useRef, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; +import { closeTabs, makeTabPermanent } from 'providers/ReduxStore/slices/tabs'; +import { deleteRequestDraft } from 'providers/ReduxStore/slices/collections'; +import { saveRequest } from 'providers/ReduxStore/slices/collections/actions'; +import { hasExampleChanges, findItemInCollection } from 'utils/collections'; +import ExampleIcon from 'components/Icons/ExampleIcon'; +import ConfirmRequestClose from '../RequestTab/ConfirmRequestClose'; +import RequestTabNotFound from '../RequestTab/RequestTabNotFound'; +import StyledWrapper from '../RequestTab/StyledWrapper'; +import CloseTabIcon from '../RequestTab/CloseTabIcon'; +import DraftTabIcon from '../RequestTab/DraftTabIcon'; + +const ExampleTab = ({ tab, collection }) => { + const dispatch = useDispatch(); + const [showConfirmClose, setShowConfirmClose] = useState(false); + + const dropdownTippyRef = useRef(); + + // Get item and example data + const item = findItemInCollection(collection, tab.itemUid); + const example = useMemo(() => item?.examples?.find((ex) => ex.uid === tab.uid), [item?.examples, tab.uid]); + + const hasChanges = useMemo(() => hasExampleChanges(item, tab.uid), [item, tab.uid]); + + const handleCloseClick = (event) => { + event.stopPropagation(); + event.preventDefault(); + dispatch(closeTabs({ + tabUids: [tab.uid] + })); + }; + + const handleRightClick = (_event) => { + const menuDropdown = dropdownTippyRef.current; + if (!menuDropdown) { + return; + } + + if (menuDropdown.state.isShown) { + menuDropdown.hide(); + } else { + menuDropdown.show(); + } + }; + + const handleMouseUp = (e) => { + if (e.button === 1) { + e.preventDefault(); + e.stopPropagation(); + + // Close the tab + dispatch(closeTabs({ + tabUids: [tab.uid] + })); + } + }; + + if (!item || !example) { + return ( + { + if (e.button === 1) { + e.preventDefault(); + e.stopPropagation(); + + dispatch(closeTabs({ tabUids: [tab.uid] })); + } + }} + > + + + ); + } + + return ( + + {showConfirmClose && ( + setShowConfirmClose(false)} + onCloseWithoutSave={() => { + dispatch(deleteRequestDraft({ + itemUid: item.uid, + collectionUid: collection.uid + })); + dispatch(closeTabs({ + tabUids: [tab.uid] + })); + setShowConfirmClose(false); + }} + onSaveAndClose={() => { + // For examples, we don't have a separate save action + // The changes are saved automatically when the request is saved + dispatch(saveRequest(item.uid, collection.uid)); + dispatch(closeTabs({ + tabUids: [tab.uid] + })); + setShowConfirmClose(false); + }} + /> + )} +
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 ? ( + + ) : ( + + )} +
+
+ ); +}; + +export default ExampleTab; 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 d02704636..293ebb3a6 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 71d57d940..028c9bb2c 100644 --- a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js @@ -1,4 +1,4 @@ -import React, { useCallback, useState, useRef, Fragment } from 'react'; +import React, { useCallback, useState, useRef, Fragment, useMemo } from 'react'; import get from 'lodash/get'; import { closeTabs, makeTabPermanent } from 'providers/ReduxStore/slices/tabs'; import { saveRequest } from 'providers/ReduxStore/slices/collections/actions'; @@ -7,7 +7,7 @@ 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 } from 'utils/collections'; import ConfirmRequestClose from './ConfirmRequestClose'; import RequestTabNotFound from './RequestTabNotFound'; import SpecialTab from './SpecialTab'; @@ -19,6 +19,7 @@ import CloseTabIcon from './CloseTabIcon'; import DraftTabIcon from './DraftTabIcon'; import { flattenItems } from 'utils/collections/index'; import { closeWsConnection } from 'utils/network/index'; +import ExampleTab from '../ExampleTab'; const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid }) => { const dispatch = useDispatch(); @@ -29,6 +30,8 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi const dropdownTippyRef = useRef(); const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); + const item = findItemInCollection(collection, tab.uid); + const handleCloseClick = (event) => { event.stopPropagation(); event.preventDefault(); @@ -92,7 +95,19 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi ); } - const item = findItemInCollection(collection, tab.uid); + // Handle response-example tabs specially + if (tab.type === 'response-example') { + return ( + + ); + } + const getMethodText = useCallback((item) => { if (!item) return; @@ -129,6 +144,7 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi const isWS = item.type === 'ws-request'; const method = getMethodText(item); + const hasChanges = useMemo(() => hasRequestChanges(item), [item]); return ( @@ -172,7 +188,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 (!hasChanges) return handleMouseUp(e); if (e.button === 1) { e.stopPropagation(); @@ -200,7 +216,7 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
{ - if (!item.draft) { + if (!hasChanges) { isWS && closeWsConnection(item.uid); return handleCloseClick(e); }; @@ -210,7 +226,7 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi setShowConfirmClose(true); }} > - {!item.draft ? ( + {!hasChanges ? ( ) : ( @@ -227,6 +243,7 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col const totalTabs = collectionRequestTabs.length || 0; const currentTabUid = collectionRequestTabs[tabIndex]?.uid; const currentTabItem = findItemInCollection(collection, currentTabUid); + const currentTabHasChanges = useMemo(() => hasRequestChanges(currentTabItem), [currentTabItem]); const hasLeftTabs = tabIndex !== 0; const hasRightTabs = totalTabs > tabIndex + 1; @@ -243,7 +260,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 +312,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 000000000..636d61b4a --- /dev/null +++ b/packages/bruno-app/src/components/ResponseExample/CreateExampleModal/index.js @@ -0,0 +1,102 @@ +import { useState, useEffect } from 'react'; +import Modal from 'components/Modal'; +import Portal from 'components/Portal'; + +const CreateExampleModal = ({ isOpen, onClose, onSave, title = 'Create Response Example', initialName = '' }) => { + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + const [nameError, setNameError] = useState(''); + + const handleNameChange = (e) => { + setName(e.target.value); + // Clear error when user starts typing + if (nameError) { + setNameError(''); + } + }; + + const handleConfirm = () => { + if (name.trim()) { + onSave(name.trim(), description.trim()); + // Reset form + setName(''); + setDescription(''); + setNameError(''); + } else { + setNameError('Example name is required'); + } + }; + + const handleClose = () => { + // Reset form when closing + setName(''); + setDescription(''); + setNameError(''); + onClose(); + }; + + useEffect(() => { + if (isOpen) { + setName(initialName); + setDescription(''); + setNameError(''); + } + }, [isOpen, initialName]); + + if (!isOpen) { + return null; + } + + return ( + + +
+
+ + + {nameError && ( +
+ {nameError} +
+ )} +
+ +
+ +