From a04ff3e81936af527f358b09da7cf5a319c14cd1 Mon Sep 17 00:00:00 2001 From: Pooja Date: Fri, 30 Jan 2026 16:23:51 +0530 Subject: [PATCH] =?UTF-8?q?feat(#304)=20Environments=20color=20?= =?UTF-8?q?=F0=9F=8E=A8=20(#1053)=20(#6974)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 439 ------------------ packages/bruno-app/package.json | 1 - .../src/components/ColorBadge/index.js | 24 + .../components/ColorPicker/StyledWrapper.js | 7 + .../src/components/ColorPicker/index.js | 164 +++++++ .../components/ColorRange/StyledWrapper.js | 45 ++ .../src/components/ColorRange/index.js | 23 + .../EnvironmentListContent/index.js | 5 +- .../EnvironmentSelector/StyledWrapper.js | 48 +- .../Environments/EnvironmentSelector/index.js | 352 ++++++++------ .../EnvironmentColor/index.js | 32 -- .../EnvironmentDetails/index.js | 26 +- .../EnvironmentList/StyledWrapper.js | 1 + .../EnvironmentList/index.js | 25 +- .../components/RequestTabs/StyledWrapper.js | 5 +- .../src/components/RequestTabs/index.js | 19 +- .../EnvironmentDetails/index.js | 18 +- .../EnvironmentList/StyledWrapper.js | 1 + .../EnvironmentList/index.js | 2 + .../ReduxStore/slices/collections/actions.js | 48 +- .../ReduxStore/slices/collections/index.js | 25 +- .../ReduxStore/slices/global-environments.js | 19 + packages/bruno-electron/src/ipc/collection.js | 22 + .../src/ipc/global-environments.js | 13 + .../src/store/global-environments.js | 9 + .../src/store/workspace-environments.js | 28 ++ .../src/formats/yml/parseEnvironment.ts | 3 +- .../src/formats/yml/stringifyEnvironment.ts | 5 +- .../test-results/.last-run.json | 4 + packages/bruno-lang/v2/src/jsonToEnv.js | 4 +- .../src/collection/environment.ts | 1 + .../bruno-schema/src/collections/index.js | 2 +- .../color-picker/collection/bruno.json | 5 + .../color-picker/collection/test-request.bru | 17 + .../color-picker/color-picker.spec.ts | 156 +++++++ .../init-user-data/collection-security.json | 10 + .../init-user-data/global-environments.json | 47 ++ .../init-user-data/preferences.json | 6 + 38 files changed, 908 insertions(+), 753 deletions(-) create mode 100644 packages/bruno-app/src/components/ColorBadge/index.js create mode 100644 packages/bruno-app/src/components/ColorPicker/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/ColorPicker/index.js create mode 100644 packages/bruno-app/src/components/ColorRange/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/ColorRange/index.js delete mode 100644 packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentColor/index.js create mode 100644 packages/bruno-filestore/test-results/.last-run.json create mode 100644 tests/environments/color-picker/collection/bruno.json create mode 100644 tests/environments/color-picker/collection/test-request.bru create mode 100644 tests/environments/color-picker/color-picker.spec.ts create mode 100644 tests/environments/color-picker/init-user-data/collection-security.json create mode 100644 tests/environments/color-picker/init-user-data/global-environments.json create mode 100644 tests/environments/color-picker/init-user-data/preferences.json diff --git a/package-lock.json b/package-lock.json index d64691a56..629cf368d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10422,420 +10422,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@uiw/color-convert": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@uiw/color-convert/-/color-convert-2.9.2.tgz", - "integrity": "sha512-ibw9OS29S7GlL+vDwU3p5XG3vhR7XdzUecydpZbakUeg2Td6nfsnrCAX9sbLwQ73p0abO42v+V4qRaWq+7/BjQ==", - "license": "MIT", - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0" - } - }, - "node_modules/@uiw/react-color": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@uiw/react-color/-/react-color-2.9.2.tgz", - "integrity": "sha512-nIrw4Ol6boAkr1CUcCAkrUFVrKbT9T7/0qaSDpXmiDgKYf77gbXTWTsqVuXVDCSoCn28LvurpASS4AW8oiSBtg==", - "license": "MIT", - "dependencies": { - "@uiw/color-convert": "2.9.2", - "@uiw/react-color-alpha": "2.9.2", - "@uiw/react-color-block": "2.9.2", - "@uiw/react-color-chrome": "2.9.2", - "@uiw/react-color-circle": "2.9.2", - "@uiw/react-color-colorful": "2.9.2", - "@uiw/react-color-compact": "2.9.2", - "@uiw/react-color-editable-input": "2.9.2", - "@uiw/react-color-editable-input-hsla": "2.9.2", - "@uiw/react-color-editable-input-rgba": "2.9.2", - "@uiw/react-color-github": "2.9.2", - "@uiw/react-color-hue": "2.9.2", - "@uiw/react-color-material": "2.9.2", - "@uiw/react-color-name": "2.9.2", - "@uiw/react-color-saturation": "2.9.2", - "@uiw/react-color-shade-slider": "2.9.2", - "@uiw/react-color-sketch": "2.9.2", - "@uiw/react-color-slider": "2.9.2", - "@uiw/react-color-swatch": "2.9.2", - "@uiw/react-color-wheel": "2.9.2" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@uiw/react-color-alpha": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@uiw/react-color-alpha/-/react-color-alpha-2.9.2.tgz", - "integrity": "sha512-a2ACkE2vZIS4xnN9DaRfkQtAX/t8oK5NRSbX2QeOL23WIMHP1VNs7Yq5gXB68RHYenFgvs2JHuMOxZ2mK1W5Mw==", - "license": "MIT", - "dependencies": { - "@uiw/color-convert": "2.9.2", - "@uiw/react-drag-event-interactive": "2.9.2" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@uiw/react-color-block": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@uiw/react-color-block/-/react-color-block-2.9.2.tgz", - "integrity": "sha512-0EIZTELA5pnxyMlBOFo3hrpy73db+Qeq6E+QptNfD/8izor8OvY1Uquj2VqD6gDz+iVHMELIoKxpaQ8sZR7NOg==", - "license": "MIT", - "dependencies": { - "@uiw/color-convert": "2.9.2", - "@uiw/react-color-editable-input": "2.9.2", - "@uiw/react-color-swatch": "2.9.2" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@uiw/react-color-chrome": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@uiw/react-color-chrome/-/react-color-chrome-2.9.2.tgz", - "integrity": "sha512-p7OZB7VWrkVbHxcTHsAq5U2vt3hAP3VvKEiDi592LKxS11IMnSd15ta8ngbJaXZWatqEpJSNgj12581yHtx+Bg==", - "license": "MIT", - "dependencies": { - "@uiw/color-convert": "2.9.2", - "@uiw/react-color-alpha": "2.9.2", - "@uiw/react-color-editable-input": "2.9.2", - "@uiw/react-color-editable-input-hsla": "2.9.2", - "@uiw/react-color-editable-input-rgba": "2.9.2", - "@uiw/react-color-github": "2.9.2", - "@uiw/react-color-hue": "2.9.2", - "@uiw/react-color-saturation": "2.9.2" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@uiw/react-color-circle": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@uiw/react-color-circle/-/react-color-circle-2.9.2.tgz", - "integrity": "sha512-7XaeX3LfCRkZKffHL/KtYps7I9hNpmx9sJOuwi0ML+3urToFD8s7Iuq3upYZt8REYt1Y84SBjuUqx2YYmjUEjA==", - "license": "MIT", - "dependencies": { - "@uiw/color-convert": "2.9.2", - "@uiw/react-color-swatch": "2.9.2" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@uiw/react-color-colorful": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@uiw/react-color-colorful/-/react-color-colorful-2.9.2.tgz", - "integrity": "sha512-tz/xeHayna2wpLipkZmcMgL1rmLMxfAmlOyBhUeWrpvqb9Fx59C/wL+5IYJA4rdsQvr9WyWjWmU/GhVKsEkW9w==", - "license": "MIT", - "dependencies": { - "@uiw/color-convert": "2.9.2", - "@uiw/react-color-alpha": "2.9.2", - "@uiw/react-color-hue": "2.9.2", - "@uiw/react-color-saturation": "2.9.2" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@uiw/react-color-compact": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@uiw/react-color-compact/-/react-color-compact-2.9.2.tgz", - "integrity": "sha512-fSSgRRBjkYuGRebZ7XM5XGVFvFwEyEGaV6mOhUpr3JFKhIES0/9oPbd80GDbRdj57Zxxrj76MCtd/aCFfwQSWA==", - "license": "MIT", - "dependencies": { - "@uiw/color-convert": "2.9.2", - "@uiw/react-color-editable-input": "2.9.2", - "@uiw/react-color-editable-input-rgba": "2.9.2", - "@uiw/react-color-swatch": "2.9.2" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@uiw/react-color-editable-input": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@uiw/react-color-editable-input/-/react-color-editable-input-2.9.2.tgz", - "integrity": "sha512-DY7pu12+LDRn6cxDMvsy1/quaPTxicAPz/kfODV7KBf8+Hq4rFWeJ4KS6m22IKIbQxrBQgmQG0WFJLaPeY7cPw==", - "license": "MIT", - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@uiw/react-color-editable-input-hsla": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@uiw/react-color-editable-input-hsla/-/react-color-editable-input-hsla-2.9.2.tgz", - "integrity": "sha512-kZfj3W20msLeP8/HY498rG30eBHRPAyxduhu94HLa9XggT/0ogwA9xZJZgWd6B7FYPeRlhRDY7dnF7caND63GQ==", - "license": "MIT", - "dependencies": { - "@uiw/color-convert": "2.9.2", - "@uiw/react-color-editable-input-rgba": "2.9.2" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@uiw/react-color-editable-input-rgba": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@uiw/react-color-editable-input-rgba/-/react-color-editable-input-rgba-2.9.2.tgz", - "integrity": "sha512-sQW3tSao754954aQuVK4qvn1i+KC2piE4UftaBubD3QxC02gg5VfgZRoI6AV+nLr73Ifv3mCXewjN1BcP/+x4A==", - "license": "MIT", - "dependencies": { - "@uiw/color-convert": "2.9.2", - "@uiw/react-color-editable-input": "2.9.2" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@uiw/react-color-github": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@uiw/react-color-github/-/react-color-github-2.9.2.tgz", - "integrity": "sha512-pKR94swzWxqRFUEZJBQ8WHcw6CklxLWhDyjvGpxiieAlwUAL0mlmtCcctRgsmJRuAKQlZx4WNslRgNX5aVcoZw==", - "license": "MIT", - "dependencies": { - "@uiw/color-convert": "2.9.2", - "@uiw/react-color-swatch": "2.9.2" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@uiw/react-color-hue": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@uiw/react-color-hue/-/react-color-hue-2.9.2.tgz", - "integrity": "sha512-vDGN5YCzw09BfxkQeDvAeQ7zAy141uJ3HkFk1lsXL7ha8xZ35AItE1s/C6d60vFjGdoloKShh0yA7df3pnjmxA==", - "license": "MIT", - "dependencies": { - "@uiw/color-convert": "2.9.2", - "@uiw/react-color-alpha": "2.9.2" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@uiw/react-color-material": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@uiw/react-color-material/-/react-color-material-2.9.2.tgz", - "integrity": "sha512-R9MI9IlTof/L1rdxUFQAWgAgUSNJGXQsPujo8UGpwR7o5d+A3wwybUnPBsGKRnZwDy5zW7x+lPxY46GXE9aU9Q==", - "license": "MIT", - "dependencies": { - "@uiw/color-convert": "2.9.2", - "@uiw/react-color-editable-input": "2.9.2", - "@uiw/react-color-editable-input-rgba": "2.9.2" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@uiw/react-color-name": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@uiw/react-color-name/-/react-color-name-2.9.2.tgz", - "integrity": "sha512-knqRUkAe3pv6rB+tGzaURtwQkBqjRG62YNlzUx8Ty7g+pskWpLSPiMikW+9H5sLPq7wU3ichZiygqIp4BRgQzA==", - "license": "MIT", - "dependencies": { - "colors-named": "^1.0.1", - "colors-named-hex": "^1.0.1" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0" - } - }, - "node_modules/@uiw/react-color-saturation": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@uiw/react-color-saturation/-/react-color-saturation-2.9.2.tgz", - "integrity": "sha512-w1aUU+g6Axwbr1nLvF8k/zg5v7UW8z80eH6C7w+tdiOFOQKkKQlXqeOG0IIUUIj3v/ji+yM90IuOH+Ku7zsJrg==", - "license": "MIT", - "dependencies": { - "@uiw/color-convert": "2.9.2", - "@uiw/react-drag-event-interactive": "2.9.2" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@uiw/react-color-shade-slider": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@uiw/react-color-shade-slider/-/react-color-shade-slider-2.9.2.tgz", - "integrity": "sha512-VE59rWv5ixqCN2CTpoe33j4SOCGU62bKguizx4HxgKczE/X0ySeEas8iP5XLg/4fYWl3EZN4uI+M8mNRnB0DPw==", - "license": "MIT", - "dependencies": { - "@uiw/color-convert": "2.9.2", - "@uiw/react-color-alpha": "2.9.2" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@uiw/react-color-sketch": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@uiw/react-color-sketch/-/react-color-sketch-2.9.2.tgz", - "integrity": "sha512-PEvwaqDVDdBI/+fWASBWQOvx3ows7dIcv6Z06VHgEXk2chi95Fkrbd0YUUXMcp7ESsmUK1j5ozGMLAf9Nvx6Nw==", - "license": "MIT", - "dependencies": { - "@uiw/color-convert": "2.9.2", - "@uiw/react-color-alpha": "2.9.2", - "@uiw/react-color-editable-input": "2.9.2", - "@uiw/react-color-editable-input-rgba": "2.9.2", - "@uiw/react-color-hue": "2.9.2", - "@uiw/react-color-saturation": "2.9.2", - "@uiw/react-color-swatch": "2.9.2" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@uiw/react-color-slider": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@uiw/react-color-slider/-/react-color-slider-2.9.2.tgz", - "integrity": "sha512-vsi0AwmFJpb+flF8XCkacbX+MwLGOzrDKMqR29XE5sO8ERaezoT5mmYXzXXFcjyZYIuXse4C3JT38nsmOBp1vQ==", - "license": "MIT", - "dependencies": { - "@uiw/color-convert": "2.9.2", - "@uiw/react-color-alpha": "2.9.2" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@uiw/react-color-swatch": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@uiw/react-color-swatch/-/react-color-swatch-2.9.2.tgz", - "integrity": "sha512-6zBy+E9NzZR672M2wPsbbNRqKy9Wi9jOuuxxyzov1CEZp+pPX7UwMlCX6RUhKdO0PzTSPCVQmbz5bplu5vsW0w==", - "license": "MIT", - "dependencies": { - "@uiw/color-convert": "2.9.2" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@uiw/react-color-wheel": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@uiw/react-color-wheel/-/react-color-wheel-2.9.2.tgz", - "integrity": "sha512-ayGzQyMZM3Cp+sX7LNElQ/QQWMO7YG4k/RQwVJAhxNQ+4lJ/p4LLSnI85D7NxILkk+jiXnjxRroxxZ2eJhWo+g==", - "license": "MIT", - "dependencies": { - "@uiw/color-convert": "2.9.2", - "@uiw/react-drag-event-interactive": "2.9.2" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@uiw/react-drag-event-interactive": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@uiw/react-drag-event-interactive/-/react-drag-event-interactive-2.9.2.tgz", - "integrity": "sha512-6gxQz+Ij7JkXlEOpfZhOu+Gdp/sI9VnMeDl8AJeYl3+0YXP31lXGmyb0NkNYnoUmJO+RrAf68c1raMpaDWs+Ow==", - "license": "MIT", - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, "node_modules/@usebruno/app": { "resolved": "packages/bruno-app", "link": true @@ -13760,30 +13346,6 @@ "dev": true, "license": "MIT" }, - "node_modules/colors-named": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/colors-named/-/colors-named-1.0.4.tgz", - "integrity": "sha512-R254qrKSxFJNa7QmM7vrRaz5Hygr7MIaNbXcIx7WfmlYJ9OjZQ+aczGlnKS8lLtNT0GM9aGZ4EcmNXrh5ttv6g==", - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - } - }, - "node_modules/colors-named-hex": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/colors-named-hex/-/colors-named-hex-1.0.3.tgz", - "integrity": "sha512-vhUoMdCdOKgD9Ni3p6uV3ET1JJCHzlcK6lN3/yl+6TUHinDE6HUFlmnvkh/NDZ2M9049Ipn3mX85qu6akRiC1g==", - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - } - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -30456,7 +30018,6 @@ "@tabler/icons": "^1.46.0", "@testing-library/user-event": "^14.6.1", "@tippyjs/react": "^4.2.6", - "@uiw/react-color": "^2.9.2", "@usebruno/common": "0.1.0", "@usebruno/graphql-docs": "0.1.0", "@usebruno/schema": "0.7.0", diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json index e6d45ac33..b9e12715f 100644 --- a/packages/bruno-app/package.json +++ b/packages/bruno-app/package.json @@ -18,7 +18,6 @@ "@tabler/icons": "^1.46.0", "@testing-library/user-event": "^14.6.1", "@tippyjs/react": "^4.2.6", - "@uiw/react-color": "^2.9.2", "@usebruno/common": "0.1.0", "@usebruno/graphql-docs": "0.1.0", "@usebruno/schema": "0.7.0", diff --git a/packages/bruno-app/src/components/ColorBadge/index.js b/packages/bruno-app/src/components/ColorBadge/index.js new file mode 100644 index 000000000..dc372bbee --- /dev/null +++ b/packages/bruno-app/src/components/ColorBadge/index.js @@ -0,0 +1,24 @@ +import React from 'react'; +import { useTheme } from 'providers/Theme'; + +const ColorBadge = ({ color, size = 10, showEmptyBorder = true }) => { + const sizeValue = typeof size === 'string' ? size : `${size}px`; + const { theme } = useTheme(); + + const showBorder = !color && showEmptyBorder; + + return ( +
+ ); +}; + +export default ColorBadge; diff --git a/packages/bruno-app/src/components/ColorPicker/StyledWrapper.js b/packages/bruno-app/src/components/ColorPicker/StyledWrapper.js new file mode 100644 index 000000000..3f1fc4bc0 --- /dev/null +++ b/packages/bruno-app/src/components/ColorPicker/StyledWrapper.js @@ -0,0 +1,7 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + +`; + +export default StyledWrapper; diff --git a/packages/bruno-app/src/components/ColorPicker/index.js b/packages/bruno-app/src/components/ColorPicker/index.js new file mode 100644 index 000000000..b2cd69198 --- /dev/null +++ b/packages/bruno-app/src/components/ColorPicker/index.js @@ -0,0 +1,164 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { IconBan, IconBrush } from '@tabler/icons'; +import Dropdown from 'components/Dropdown'; +import ColorBadge from 'components/ColorBadge'; +import StyledWrapper from './StyledWrapper'; +import { parseToRgb, toColorString } from 'polished'; +import ColorRangePicker from 'components/ColorRange/index'; + +const PRESET_COLORS = [ + '#CE4F3B', + '#2E8A54', + '#346AB2', + '#C77A0F', + '#B83D7F', + '#8D44B2' +]; + +const COLOR_RANGE_SEQUENCE = ['#D85D43', '#F4BB74', '#61DCB1', '#7EBDF2', '#D48ADE', '#B491E5']; + +/** + * @param {string} hex + * @returns {red:string,green:string,blue:string} + */ +const hexToRgb = (hex) => { + try { + return parseToRgb(hex); + } catch (err) { + return { red: 0, green: 0, blue: 0 }; + } +}; + +const rgbToHex = (r, g, b) => { + return toColorString({ red: Math.round(r), green: Math.round(g), blue: Math.round(b) }); +}; + +const interpolateColor = (position) => { + const numColors = COLOR_RANGE_SEQUENCE.length; + const scaledPos = (position / 100) * (numColors - 1); + const index = Math.floor(scaledPos); + const fraction = scaledPos - index; + + if (index >= numColors - 1) { + return COLOR_RANGE_SEQUENCE[numColors - 1]; + } + + const color1 = hexToRgb(COLOR_RANGE_SEQUENCE[index]); + const color2 = hexToRgb(COLOR_RANGE_SEQUENCE[index + 1]); + + const r = color1.red + (color2.red - color1.red) * fraction; + const g = color1.green + (color2.green - color1.green) * fraction; + const b = color1.blue + (color2.blue - color1.blue) * fraction; + + return rgbToHex(r, g, b); +}; + +const findClosestPosition = (hex) => { + if (!hex) return 0; + const target = hexToRgb(hex); + let closestPos = 0; + let minDistance = Infinity; + + for (let pos = 0; pos <= 100; pos++) { + const color = hexToRgb(interpolateColor(pos)); + const distance = Math.sqrt( + Math.pow(target.red - color.red, 2) + Math.pow(target.green - color.green, 2) + Math.pow(target.blue - color.blue, 2) + ); + if (distance < minDistance) { + minDistance = distance; + closestPos = pos; + } + } + return closestPos; +}; + +const ColorPickerIcon = ({ color }) => { + if (color) { + return ; + } + return ; +}; + +const ColorPicker = ({ color, onChange, icon }) => { + const [sliderPosition, setSliderPosition] = useState(() => + color && !PRESET_COLORS.includes(color) ? findClosestPosition(color) : 0 + ); + const [customColor, setCustomColor] = useState(() => + color && !PRESET_COLORS.includes(color) ? color : COLOR_RANGE_SEQUENCE[0] + ); + const pendingColorRef = useRef(customColor); + + const handleColorSelect = (selectedColor) => { + onChange(selectedColor); + }; + + const handleSliderChange = (e) => { + const newPosition = parseInt(e.target.value, 10); + setSliderPosition(newPosition); + const newColor = interpolateColor(newPosition); + setCustomColor(newColor); + pendingColorRef.current = newColor; + }; + + const handleSliderEnd = () => { + onChange(pendingColorRef.current); + }; + + const defaultIcon = ( +
+ +
+ ); + + const colorPickerContent = ( + +
+
+
handleColorSelect(null)} + title="No color" + > + +
+ {PRESET_COLORS.map((presetColor, index) => ( +
handleColorSelect(presetColor)} + title={presetColor} + /> + ))} +
+ +
+
handleColorSelect(customColor)} + title="Custom color" + /> + +
+
+ + ); + + return ( + + {colorPickerContent} + + ); +}; + +export default ColorPicker; diff --git a/packages/bruno-app/src/components/ColorRange/StyledWrapper.js b/packages/bruno-app/src/components/ColorRange/StyledWrapper.js new file mode 100644 index 000000000..7d5addb9a --- /dev/null +++ b/packages/bruno-app/src/components/ColorRange/StyledWrapper.js @@ -0,0 +1,45 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + .hue-slider { + -webkit-appearance: none; + appearance: none; + height: 4px; + border-radius: 2px; + outline: none; + } + + .hue-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 14px; + height: 14px; + border-radius: 50%; + background: ${(props) => props.color ?? props.theme.bg}; + border: none; + cursor: pointer; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + transition: transform 0.1s ease; + } + + .hue-slider::-webkit-slider-thumb:hover { + transform: scale(1.1); + } + + .hue-slider::-moz-range-thumb { + width: 14px; + height: 14px; + border-radius: 50%; + background: ${(props) => props.color ?? props.theme.bg}; + border: none; + cursor: pointer; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + transition: transform 0.1s ease; + } + + .hue-slider::-moz-range-thumb:hover { + transform: scale(1.1); + } +`; + +export default StyledWrapper; diff --git a/packages/bruno-app/src/components/ColorRange/index.js b/packages/bruno-app/src/components/ColorRange/index.js new file mode 100644 index 000000000..09d14f50c --- /dev/null +++ b/packages/bruno-app/src/components/ColorRange/index.js @@ -0,0 +1,23 @@ +import StyledWrapper from './StyledWrapper'; + +const ColorRangePicker = ({ selectedColor, className, value, onChange, colorRange, ...props }) => { + return ( + + + + ); +}; + +export default ColorRangePicker; diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSelector/EnvironmentListContent/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSelector/EnvironmentListContent/index.js index 217670a58..5685ddee8 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSelector/EnvironmentListContent/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSelector/EnvironmentListContent/index.js @@ -1,6 +1,7 @@ import React from 'react'; -import { IconPlus, IconDownload, IconSettings, IconDatabase } from '@tabler/icons'; +import { IconPlus, IconDownload, IconSettings } from '@tabler/icons'; import ToolHint from 'components/ToolHint'; +import ColorBadge from 'components/ColorBadge'; const EnvironmentListContent = ({ environments, @@ -38,7 +39,7 @@ const EnvironmentListContent = ({ data-tooltip-content={env.name} data-tooltip-hidden={env.name?.length < 90} > - + {env.name}
))} diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSelector/StyledWrapper.js b/packages/bruno-app/src/components/Environments/EnvironmentSelector/StyledWrapper.js index 0bfb7befe..082c7a50f 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSelector/StyledWrapper.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSelector/StyledWrapper.js @@ -5,8 +5,8 @@ const Wrapper = styled.div` border-radius: ${(props) => props.theme.border.radius.base}; padding: 0.25rem 0.3rem 0.25rem 0.5rem; user-select: none; - background-color: ${(props) => props.color ? undefined : 'transparent'}; - border: 2px solid ${(props) => props.color ?? props.theme.dropdown.selectedColor}; + background-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.bg}; + border: 1px solid ${(props) => props.theme.app.collection.toolbar.environmentSelector.border}; line-height: 1rem; transition: all 0.15s ease; @@ -15,11 +15,6 @@ const Wrapper = styled.div` background-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.hoverBg}; } - .active-env-toolhint { - display: flex; - flex-direction: row; - } - .caret { margin-left: 0.25rem; color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.caret}; @@ -29,18 +24,16 @@ const Wrapper = styled.div` .env-icon { margin-right: 0.25rem; - color: ${(props) => props.color ?? props.theme.dropdown.selectedColor}; + color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.icon}; } .env-text { - color: ${(props) => props.color ?? props.theme.dropdown.selectedColor}; - font-size: ${(props) => props.theme.font.size.base}; + color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.text}; display: block; } .env-separator { - color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.separator}; - margin: 0 0.35rem; + background-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.separator}; } .env-text-inactive { @@ -71,37 +64,6 @@ const Wrapper = styled.div` overflow: hidden; } - .tippy-box .tippy-content { - padding: 0; - display: flex; - flex-direction: column; - height: 100%; - - .dropdown-item { - display: flex; - flex-direction: row; - align-items: center; - column-gap: 0.35em; - padding: 0.35rem 0.6rem; - cursor: pointer; - font-size: ${(props) => props.theme.font.size.base}; - color: ${(props) => props.theme.dropdown.primaryText}; - - &:hover:not(:disabled) { - background-color: ${(props) => props.theme.dropdown.hoverBg}; - } - - &.active { - background-color: ${(props) => props.theme.dropdown.selectedBg}; - color: ${(props) => props.color ?? props.theme.dropdown.selectedColor} !important; - } - - &.no-environment { - color: ${(props) => props.theme.dropdown.mutedText}; - } - } - } - .configure-button { position: absolute; bottom: 0; diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js index 0ede0ae96..3fc299f72 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js @@ -13,6 +13,166 @@ import ImportEnvironmentModal from 'components/Environments/Common/ImportEnviron import CreateGlobalEnvironment from 'components/WorkspaceHome/WorkspaceEnvironments/CreateEnvironment'; import ToolHint from 'components/ToolHint'; import StyledWrapper from './StyledWrapper'; +import { transparentize, toColorString, parseToRgb } from 'polished'; + +const TABS = [ + { id: 'collection', label: 'Collection', icon: }, + { id: 'global', label: 'Global', icon: } +]; + +const EMPTY_STATE_DESCRIPTIONS = { + collection: 'Create your first environment to begin working with your collection.', + global: 'Create your first global environment to begin working across collections.' +}; + +/** + * Generates background color with transparency for environment badges + */ +const getEnvBackgroundColor = (color) => (color ? transparentize(1 - 0.12, color) : 'transparent'); + +/** + * Calculates the style for an environment badge section + */ +const getEnvBadgeStyle = (environment, position, hasOtherEnv) => { + const color = environment?.color; + const isLeft = position === 'left'; + + // Determine border radius based on position and whether other env exists + let borderRadius = '0.3rem'; + if (hasOtherEnv) { + borderRadius = isLeft ? '0.3rem 0 0 0.3rem' : '0 0.3rem 0.3rem 0'; + } + + // Determine padding based on position + const padding = isLeft + ? hasOtherEnv + ? '0.25rem 0.5rem 0.25rem 0.5rem' + : '0.25rem 0.3rem 0.25rem 0.5rem' + : '0.25rem 0.3rem 0.25rem 0.5rem'; + + return { + backgroundColor: getEnvBackgroundColor(color), + padding, + borderRadius + }; +}; + +/** + * Calculates dropdown width based on longest environment name + */ +const calculateDropdownWidth = (environments, globalEnvironments) => { + const allEnvironments = [...environments, ...globalEnvironments]; + if (allEnvironments.length === 0) return 0; + + const maxCharLength = Math.max(...allEnvironments.map((env) => env.name?.length || 0)); + // 8 pixels per character (rough estimate for average character width) + return maxCharLength * 8; +}; + +/** + * Displays a single environment with icon, name, and optional color styling + */ +const EnvironmentBadge = ({ environment, icon: Icon }) => { + if (!environment) return null; + + const colorStyle = environment.color ? { color: environment.color } : {}; + + return ( + <> + + + + ); +}; + +/** + * Dropdown trigger component showing active environments + */ +const DropdownTrigger = forwardRef(({ collectionEnv, globalEnv }, ref) => { + const hasAnyEnv = collectionEnv || globalEnv; + + // Empty state - no environments selected + if (!hasAnyEnv) { + return ( +
+ No Environment + +
+ ); + } + + // Only collection env selected - caret goes with collection env + if (collectionEnv && !globalEnv) { + return ( +
+
+ + +
+
+ ); + } + + // Only global env selected - caret goes with global env + if (!collectionEnv && globalEnv) { + return ( +
+
+ + +
+
+ ); + } + + // Both environments selected + return ( +
+ {/* Collection Environment Section */} +
+ +
+ + {/* Separator */} +
+ + {/* Global Environment Section + Caret */} +
+ + +
+
+ ); +}); const EnvironmentSelector = ({ collection }) => { const dispatch = useDispatch(); @@ -35,160 +195,82 @@ const EnvironmentSelector = ({ collection }) => { ? find(environments, (e) => e.uid === activeEnvironmentUid) : null; - const tabs = [ - { id: 'collection', label: 'Collection', icon: }, - { id: 'global', label: 'Global', icon: } - ]; + const dropdownWidth = useMemo( + () => calculateDropdownWidth(environments, globalEnvironments), + [environments, globalEnvironments] + ); - const onDropdownCreate = (ref) => { - dropdownTippyRef.current = ref; - }; + const description = EMPTY_STATE_DESCRIPTIONS[activeTab]; - // Get description based on active tab - const description - = activeTab === 'collection' - ? 'Create your first environment to begin working with your collection.' - : 'Create your first global environment to begin working across collections.'; + const hideDropdown = () => dropdownTippyRef.current?.hide(); - // Environment selection handler const handleEnvironmentSelect = (environment) => { const action = activeTab === 'collection' - ? selectEnvironment(environment ? environment.uid : null, collection.uid) - : selectGlobalEnvironment({ environmentUid: environment ? environment.uid : null }); + ? selectEnvironment(environment?.uid || null, collection.uid) + : selectGlobalEnvironment({ environmentUid: environment?.uid || null }); dispatch(action) .then(() => { - if (environment) { - toast.success(`Environment changed to ${environment.name}`); - } else { - toast.success('No Environments are active now'); - } - dropdownTippyRef.current.hide(); + toast.success(environment ? `Environment changed to ${environment.name}` : 'No Environments are active now'); + hideDropdown(); }) - .catch((err) => { + .catch(() => { toast.error('An error occurred while selecting the environment'); }); }; - // Settings handler - opens environment settings tab const handleSettingsClick = () => { - if (activeTab === 'collection') { - dispatch( - addTab({ - uid: `${collection.uid}-environment-settings`, - collectionUid: collection.uid, - type: 'environment-settings' - }) - ); - } else { - dispatch( - addTab({ - uid: `${collection.uid}-global-environment-settings`, - collectionUid: collection.uid, - type: 'global-environment-settings' - }) - ); - } - dropdownTippyRef.current.hide(); + const isCollection = activeTab === 'collection'; + dispatch( + addTab({ + uid: `${collection.uid}-${isCollection ? 'environment' : 'global-environment'}-settings`, + collectionUid: collection.uid, + type: isCollection ? 'environment-settings' : 'global-environment-settings' + }) + ); + hideDropdown(); }; - // Create handler const handleCreateClick = () => { if (activeTab === 'collection') { setShowCreateCollectionModal(true); } else { setShowCreateGlobalModal(true); } - dropdownTippyRef.current.hide(); + hideDropdown(); }; - // Import handler const handleImportClick = () => { if (activeTab === 'collection') { setShowImportCollectionModal(true); } else { setShowImportGlobalModal(true); } - dropdownTippyRef.current.hide(); + hideDropdown(); }; - // Calculate dropdown width based on the longest environment name. - // To prevent resizing while switching between collection and global environments. - const dropdownWidth = useMemo(() => { - const allEnvironments = [...environments, ...globalEnvironments]; - if (allEnvironments.length === 0) return 0; - - const maxCharLength = Math.max(...allEnvironments.map((env) => env.name?.length || 0)); - // 8 pixels per character: This is a rough estimate for the average character width in most fonts - // (monospace fonts are typically 8-10px, proportional fonts vary but 8px is a safe average) - return maxCharLength * 8; - }, [environments, globalEnvironments]); - - // Create icon component for dropdown trigger - const Icon = forwardRef((props, ref) => { - const hasAnyEnv = activeGlobalEnvironment || activeCollectionEnvironment; - - const displayContent = hasAnyEnv ? ( - <> - {activeCollectionEnvironment && ( - <> -
- -
- {activeGlobalEnvironment && |} - - )} - {activeGlobalEnvironment && ( -
- - -
- )} - - ) : ( - No Environment + const openEnvironmentSettingsTab = (type) => { + dispatch( + addTab({ + uid: `${collection.uid}-${type}-settings`, + collectionUid: collection.uid, + type: `${type}-settings` + }) ); - - return ( -
- {displayContent} - -
- ); - }); + }; return ( - +
- } placement="bottom-end"> + (dropdownTippyRef.current = ref)} + icon={} + placement="bottom-end" + > {/* Tab Headers */}
- {tabs.map((tab) => ( + {TABS.map((tab) => (
- - +
+ +
); }; diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/StyledWrapper.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/StyledWrapper.js index 920386fa1..542b93f5f 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/StyledWrapper.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/StyledWrapper.js @@ -110,6 +110,7 @@ const StyledWrapper = styled.div` display: flex; align-items: center; justify-content: space-between; + gap: 8px; padding: 4px 8px; margin-bottom: 1px; font-size: 13px; diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/index.js index 68eb2c02e..c7f40a13f 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/index.js @@ -1,5 +1,4 @@ import React, { useEffect, useState, useRef } from 'react'; -import { findEnvironmentInCollection, findItem } from 'utils/collections'; import usePrevious from 'hooks/usePrevious'; import EnvironmentDetails from './EnvironmentDetails'; import CreateEnvironment from 'components/Environments/EnvironmentSettings/CreateEnvironment'; @@ -7,6 +6,7 @@ import { IconDownload, IconUpload, IconSearch, IconPlus, IconCheck, IconX } from import StyledWrapper from './StyledWrapper'; import ConfirmSwitchEnv from 'components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/ConfirmSwitchEnv'; import ImportEnvironmentModal from 'components/Environments/Common/ImportEnvironmentModal'; +import ColorBadge from 'components/ColorBadge'; import { isEqual } from 'lodash'; import { useDispatch } from 'react-redux'; import { addEnvironment, renameEnvironment, selectEnvironment } from 'providers/ReduxStore/slices/collections/actions'; @@ -25,10 +25,6 @@ const EnvironmentList = ({ }) => { const dispatch = useDispatch(); - -const EnvironmentList = ({ collection, isModified, setIsModified, onClose, setShowExportModal }) => { - const { environments, activeEnvironmentUid } = collection; - const [selectedEnvironment, setSelectedEnvironment] = useState(null); const [openCreateModal, setOpenCreateModal] = useState(false); const [openImportModal, setOpenImportModal] = useState(false); const [searchText, setSearchText] = useState(''); @@ -43,7 +39,7 @@ const EnvironmentList = ({ collection, isModified, setIsModified, onClose, setSh const [switchEnvConfirmClose, setSwitchEnvConfirmClose] = useState(false); const [originalEnvironmentVariables, setOriginalEnvironmentVariables] = useState([]); - const envUids = environments?.map((env) => env.uid) ?? []; + const envUids = environments ? environments.map((env) => env.uid) : []; const prevEnvUids = usePrevious(envUids); useEffect(() => { @@ -68,12 +64,10 @@ const EnvironmentList = ({ collection, isModified, setIsModified, onClose, setSh if (hasSelectedEnvironmentChanged || selectedEnvironment.uid !== _selectedEnvironment?.uid) { setSelectedEnvironment(_selectedEnvironment); } - setOriginalEnvironmentVariables(selectedEnvironment?.variables||[]); - setSelectedEnvironment(findItem(environments, selectedEnvironment.uid)); + setOriginalEnvironmentVariables(_selectedEnvironment?.variables || []); return; } - const environment = environments?.find((env) => env.uid === activeEnvironmentUid) || environments?.[0]; setSelectedEnvironment(environment); @@ -81,21 +75,15 @@ const EnvironmentList = ({ collection, isModified, setIsModified, onClose, setSh }, [environments, activeEnvironmentUid, selectedEnvironment]); useEffect(() => { - if (selectedEnvironment) { - setSelectedEnvironment(findEnvironmentInCollection(collection, selectedEnvironment.uid)); - } - }, [environments]); - - useEffect(() => { - if (prevEnvUids?.length && envUids.length > prevEnvUids.length) { + if (prevEnvUids && prevEnvUids.length && envUids.length > prevEnvUids.length) { const newEnv = environments.find((env) => !prevEnvUids.includes(env.uid)); if (newEnv) { setSelectedEnvironment(newEnv); } } - if (prevEnvUids?.length && envUids.length < prevEnvUids.length) { - setSelectedEnvironment(environments?.length ? environments[0] : null); + if (prevEnvUids && prevEnvUids.length && envUids.length < prevEnvUids.length) { + setSelectedEnvironment(environments && environments.length ? environments[0] : null); } }, [envUids, environments, prevEnvUids]); @@ -380,6 +368,7 @@ const EnvironmentList = ({ collection, isModified, setIsModified, onClose, setSh
) : ( <> + {env.name}
{activeEnvironmentUid === env.uid ? ( diff --git a/packages/bruno-app/src/components/RequestTabs/StyledWrapper.js b/packages/bruno-app/src/components/RequestTabs/StyledWrapper.js index 28fc42862..ee8197ede 100644 --- a/packages/bruno-app/src/components/RequestTabs/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestTabs/StyledWrapper.js @@ -71,8 +71,9 @@ const Wrapper = styled.div` &:not(.active) { background: ${(props) => props.theme.requestTabs.bg}; - border-bottom: 3px solid ${(props) => props.color ?? "transparent"}; + border-color: transparent; border-radius: ${(props) => props.theme.border.radius.base}; + } &:nth-last-child(1) { @@ -112,7 +113,7 @@ const Wrapper = styled.div` &.active { background: ${(props) => props.theme.bg || '#ffffff'}; border: 1px solid ${(props) => props.theme.requestTabs.bottomBorder}; - border-bottom-color: ${(props) => props.color ?? props.theme.bg ?? '#ffffff'}; + border-bottom-color: ${(props) => props.theme.bg || '#ffffff'}; border-radius: 8px 8px 0 0; z-index: 1; margin-bottom: -2px; diff --git a/packages/bruno-app/src/components/RequestTabs/index.js b/packages/bruno-app/src/components/RequestTabs/index.js index ae97c3479..b10552008 100644 --- a/packages/bruno-app/src/components/RequestTabs/index.js +++ b/packages/bruno-app/src/components/RequestTabs/index.js @@ -6,7 +6,6 @@ import { IconChevronRight, IconChevronLeft } from '@tabler/icons'; import { useSelector, useDispatch } from 'react-redux'; import { focusTab, reorderTabs } from 'providers/ReduxStore/slices/tabs'; import NewRequest from 'components/Sidebar/NewRequest'; -import { findEnvironmentInCollection } from 'utils/collections'; import CollectionToolBar from './CollectionToolBar'; import RequestTab from './RequestTab'; import StyledWrapper from './StyledWrapper'; @@ -86,17 +85,6 @@ const RequestTabs = () => { return null; } - const activeTab = find(tabs, (t) => t.uid === activeTabUid); - if (!activeTab) { - return Something went wrong!; - } - - const activeCollection = find(collections, (c) => c.uid === activeTab.collectionUid); - const activeEnvironment = activeCollection - ? findEnvironmentInCollection(activeCollection, activeCollection.activeEnvironmentUid) - : null; - const collectionRequestTabs = filter(tabs, (t) => t.collectionUid === activeTab.collectionUid); - const effectiveSidebarWidth = sidebarCollapsed ? 0 : leftSidebarWidth; const maxTablistWidth = screenWidth - effectiveSidebarWidth - 150; @@ -114,14 +102,9 @@ const RequestTabs = () => { }); }; - const getRootClassname = () => { - return classnames({ - 'has-chevrons': showChevrons - }); - }; // Todo: Must support ephemeral requests return ( - + {newRequestModalOpen && ( setNewRequestModalOpen(false)} /> )} diff --git a/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/EnvironmentDetails/index.js b/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/EnvironmentDetails/index.js index 362e33ac6..9f7d2705c 100644 --- a/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/EnvironmentDetails/index.js +++ b/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/EnvironmentDetails/index.js @@ -1,12 +1,13 @@ import { IconCopy, IconEdit, IconTrash, IconCheck, IconX } from '@tabler/icons'; import { useState, useRef } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { renameGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments'; +import { renameGlobalEnvironment, updateGlobalEnvironmentColor } from 'providers/ReduxStore/slices/global-environments'; import { validateName, validateNameError } from 'utils/common/regex'; import toast from 'react-hot-toast'; import CopyEnvironment from '../../CopyEnvironment'; import DeleteEnvironment from '../../DeleteEnvironment'; import EnvironmentVariables from './EnvironmentVariables'; +import ColorPicker from 'components/ColorPicker'; import StyledWrapper from './StyledWrapper'; const EnvironmentDetails = ({ environment, setIsModified, collection }) => { @@ -110,6 +111,16 @@ const EnvironmentDetails = ({ environment, setIsModified, collection }) => { } }; + const handleColorChange = (color) => { + dispatch(updateGlobalEnvironmentColor(environment.uid, color)) + .then(() => { + toast.success('Environment color updated!'); + }) + .catch(() => { + toast.error('An error occurred while updating the environment color'); + }); + }; + return ( {openDeleteModal && ( @@ -159,7 +170,10 @@ const EnvironmentDetails = ({ environment, setIsModified, collection }) => {
) : ( -

{environment.name}

+
+

{environment.name}

+ +
)}
{nameError && isRenaming &&
{nameError}
} diff --git a/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/StyledWrapper.js b/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/StyledWrapper.js index ead76685a..cbf76f605 100644 --- a/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/StyledWrapper.js +++ b/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/StyledWrapper.js @@ -110,6 +110,7 @@ const StyledWrapper = styled.div` display: flex; align-items: center; justify-content: space-between; + gap: 8px; padding: 4px 8px; margin-bottom: 1px; font-size: 13px; diff --git a/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/index.js b/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/index.js index 7ce4c66ef..ed42867c7 100644 --- a/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/index.js +++ b/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/index.js @@ -6,6 +6,7 @@ import { IconDownload, IconUpload, IconSearch, IconPlus, IconCheck, IconX } from import StyledWrapper from './StyledWrapper'; import ConfirmSwitchEnv from './ConfirmSwitchEnv'; import ImportEnvironmentModal from 'components/Environments/Common/ImportEnvironmentModal'; +import ColorBadge from 'components/ColorBadge'; import { isEqual } from 'lodash'; import { useDispatch, useSelector } from 'react-redux'; import { addGlobalEnvironment, renameGlobalEnvironment, selectGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments'; @@ -357,6 +358,7 @@ const EnvironmentList = ({ environments, activeEnvironmentUid, selectedEnvironme
) : ( <> + {env.name}
{activeEnvironmentUid === env.uid ? ( diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index da06e38c4..f3f3cfef0 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -36,7 +36,6 @@ import { sortCollections as _sortCollections, updateCollectionMountStatus, moveCollection, - saveEnvironmentColor as _saveEnvironmentColor, workspaceEnvUpdateEvent, requestCancelled, resetRunResults, @@ -50,6 +49,7 @@ import { updateActiveConnections, saveRequest as _saveRequest, saveEnvironment as _saveEnvironment, + updateEnvironmentColor as _updateEnvironmentColor, saveCollectionDraft, saveFolderDraft, addVar, @@ -1931,6 +1931,31 @@ export const saveEnvironment = (variables, environmentUid, collectionUid) => (di }); }; +export const updateEnvironmentColor = (environmentUid, color, collectionUid) => (dispatch, getState) => { + return new Promise((resolve, reject) => { + const state = getState(); + const collection = findCollectionByUid(state.collections.collections, collectionUid); + if (!collection) { + return reject(new Error('Collection not found')); + } + + const collectionCopy = cloneDeep(collection); + const environment = findEnvironmentInCollection(collectionCopy, environmentUid); + if (!environment) { + return reject(new Error('Environment not found')); + } + + environment.color = color; + const { ipcRenderer } = window; + ipcRenderer.invoke('renderer:update-environment-color', collection.pathname, environment.name, color) + .then(() => { + dispatch(_updateEnvironmentColor({ environmentUid, color, collectionUid })); + resolve(); + }) + .catch(reject); + }); +}; + /** * Update a variable value directly in the file without affecting draft state * @param {string} pathname - File path @@ -2253,27 +2278,6 @@ export const mergeAndPersistEnvironment }); }; -export const saveEnvironmentColor = (color, environmentUid, collectionUid) => (dispatch, getState) => { - return new Promise((resolve, reject) => { - const collection = - findCollectionByUid(getState().collections.collections, collectionUid) ?? - reject(new Error('Collection not found')); - const environment = - findEnvironmentInCollection(collection, environmentUid) ?? reject(new Error('Environment not found')); - const updatedEnvironment = { ...environment, color: color }; - - const { ipcRenderer } = window; - environmentSchema - .validate(updatedEnvironment) - // save to file - .then(() => ipcRenderer.invoke('renderer:save-environment', collection.pathname, updatedEnvironment)) - // update store - .then(() => dispatch(_saveEnvironmentColor({ color, environmentUid, collectionUid }))) - .then(resolve) - .catch(reject); - }); -}; - export const selectEnvironment = (environmentUid, collectionUid) => (dispatch, getState) => { return new Promise((resolve, reject) => { const state = getState(); diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index 37bd9cb03..efec9f44a 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -258,17 +258,6 @@ export const collectionsSlice = createSlice({ if (environment) { environment.variables = variables; - environment.color = color; - } - } - }, - saveEnvironmentColor: (state, action) => { - const { color, environmentUid, collectionUid } = action.payload; - const collection = findCollectionByUid(state.collections, collectionUid); - if (collection) { - const environment = findEnvironmentInCollection(collection, environmentUid); - if (environment) { - environment.color = color; } } }, @@ -288,6 +277,18 @@ export const collectionsSlice = createSlice({ } } }, + updateEnvironmentColor: (state, action) => { + const { environmentUid, color, collectionUid } = action.payload; + const collection = findCollectionByUid(state.collections, collectionUid); + + if (collection) { + const environment = findEnvironmentInCollection(collection, environmentUid); + + if (environment) { + environment.color = color; + } + } + }, newItem: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); @@ -3475,8 +3476,8 @@ export const { updatedFolderSettingsSelectedTab, collectionUnlinkEnvFileEvent, saveEnvironment, - saveEnvironmentColor, selectEnvironment, + updateEnvironmentColor, newItem, deleteItem, renameItem, diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js b/packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js index 028c0db29..5eb823ada 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js @@ -81,6 +81,12 @@ export const globalEnvironmentsSlice = createSlice({ }, clearGlobalEnvironmentDraft: (state) => { state.globalEnvironmentDraft = null; + }, + _updateGlobalEnvironmentColor: (state, action) => { + const { environmentUid, color } = action.payload; + if (environmentUid) { + state.globalEnvironments = state.globalEnvironments.map((env) => env?.uid == environmentUid ? { ...env, color } : env); + } } } }); @@ -93,6 +99,7 @@ export const { _copyGlobalEnvironment, _selectGlobalEnvironment, _deleteGlobalEnvironment, + _updateGlobalEnvironmentColor, setGlobalEnvironmentDraft, clearGlobalEnvironmentDraft } = globalEnvironmentsSlice.actions; @@ -303,4 +310,16 @@ export const globalEnvironmentsUpdateEvent = ({ globalEnvironmentVariables }) => }); }; +export const updateGlobalEnvironmentColor = (environmentUid, color) => (dispatch, getState) => { + return new Promise((resolve, reject) => { + const { ipcRenderer } = window; + const state = getState(); + const { workspaceUid, workspacePath } = getWorkspaceContext(state); + ipcRenderer.invoke('renderer:update-global-environment-color', { environmentUid, color, workspaceUid, workspacePath }) + .then(() => dispatch(_updateGlobalEnvironmentColor({ environmentUid, color }))) + .then(resolve) + .catch(reject); + }); +}; + export default globalEnvironmentsSlice.reducer; diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index eb465d8a1..e72e41195 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -620,6 +620,28 @@ const registerRendererEventHandlers = (mainWindow, watcher) => { } }); + // update environment color + ipcMain.handle('renderer:update-environment-color', async (event, collectionPathname, environmentName, color) => { + try { + const format = getCollectionFormat(collectionPathname); + const envDirPath = path.join(collectionPathname, 'environments'); + const envFilePath = path.join(envDirPath, `${environmentName}.${format}`); + + if (!fs.existsSync(envFilePath)) { + throw new Error(`environment: ${envFilePath} does not exist`); + } + + // Read, update color, and write back to file + const fileContent = fs.readFileSync(envFilePath, 'utf8'); + const environment = parseEnvironment(fileContent, { format }); + environment.color = color; + const updatedContent = stringifyEnvironment(environment, { format }); + fs.writeFileSync(envFilePath, updatedContent, 'utf8'); + } catch (error) { + return Promise.reject(error); + } + }); + // Generic environment export handler ipcMain.handle('renderer:export-environment', async (event, { environments, environmentType, filePath, exportFormat = 'folder' }) => { try { diff --git a/packages/bruno-electron/src/ipc/global-environments.js b/packages/bruno-electron/src/ipc/global-environments.js index 2432e283e..432fd7330 100644 --- a/packages/bruno-electron/src/ipc/global-environments.js +++ b/packages/bruno-electron/src/ipc/global-environments.js @@ -99,6 +99,19 @@ const registerGlobalEnvironmentsIpc = (mainWindow, workspaceEnvironmentsManager) return Promise.reject(error); } }); + + ipcMain.handle('renderer:update-global-environment-color', async (event, { environmentUid, color, workspacePath }) => { + try { + if (workspacePath && workspaceEnvironmentsManager) { + return await workspaceEnvironmentsManager.updateGlobalEnvironmentColorByPath(workspacePath, { environmentUid, color }); + } + + globalEnvironmentsStore.updateGlobalEnvironmentColor({ environmentUid, color }); + } catch (error) { + console.error('Error in renderer:update-global-environment-color:', error); + return Promise.reject(error); + } + }); }; module.exports = registerGlobalEnvironmentsIpc; diff --git a/packages/bruno-electron/src/store/global-environments.js b/packages/bruno-electron/src/store/global-environments.js index 0bdeb6957..084803af3 100644 --- a/packages/bruno-electron/src/store/global-environments.js +++ b/packages/bruno-electron/src/store/global-environments.js @@ -162,6 +162,15 @@ class GlobalEnvironmentsStore { } this.setGlobalEnvironments(globalEnvironments); } + + updateGlobalEnvironmentColor({ environmentUid, color }) { + let globalEnvironments = this.getGlobalEnvironments(); + const environment = globalEnvironments.find((env) => env?.uid == environmentUid); + if (environment) { + environment.color = color; + } + this.setGlobalEnvironments(globalEnvironments); + } } const globalEnvironmentsStore = new GlobalEnvironmentsStore(); diff --git a/packages/bruno-electron/src/store/workspace-environments.js b/packages/bruno-electron/src/store/workspace-environments.js index b27ad67b4..3098f2b35 100644 --- a/packages/bruno-electron/src/store/workspace-environments.js +++ b/packages/bruno-electron/src/store/workspace-environments.js @@ -325,6 +325,30 @@ class GlobalEnvironmentsManager { } } + async updateGlobalEnvironmentColor(workspacePath, environmentUid, color) { + try { + if (!workspacePath) { + throw new Error('Workspace path is required'); + } + + const envFile = this.findEnvironmentFileByUid(workspacePath, environmentUid); + + if (!envFile) { + throw new Error(`Environment file not found for uid: ${environmentUid}`); + } + + const environment = await this.parseEnvironmentFile(envFile.filePath, workspacePath); + environment.color = color; + + const content = stringifyEnvironment(environment, { format: 'yml' }); + await writeFile(envFile.filePath, content); + + return true; + } catch (error) { + throw error; + } + } + async getGlobalEnvironmentsByPath(workspacePath) { return this.getGlobalEnvironments(workspacePath); } @@ -348,6 +372,10 @@ class GlobalEnvironmentsManager { async selectGlobalEnvironmentByPath(workspacePath, params) { return this.selectGlobalEnvironment(workspacePath, params); } + + async updateGlobalEnvironmentColorByPath(workspacePath, { environmentUid, color }) { + return this.updateGlobalEnvironmentColor(workspacePath, environmentUid, color); + } } const globalEnvironmentsManager = new GlobalEnvironmentsManager(); diff --git a/packages/bruno-filestore/src/formats/yml/parseEnvironment.ts b/packages/bruno-filestore/src/formats/yml/parseEnvironment.ts index 5eaba6baf..92b4016b8 100644 --- a/packages/bruno-filestore/src/formats/yml/parseEnvironment.ts +++ b/packages/bruno-filestore/src/formats/yml/parseEnvironment.ts @@ -43,7 +43,8 @@ const parseEnvironment = (ymlString: string): BrunoEnvironment => { const brunoEnvironment: BrunoEnvironment = { uid: uuid(), name: ensureString(ocEnvironment.name, 'Untitled Environment'), - variables: toBrunoEnvironmentVariables(ocEnvironment.variables) + variables: toBrunoEnvironmentVariables(ocEnvironment.variables), + color: ocEnvironment.color || null }; return brunoEnvironment; diff --git a/packages/bruno-filestore/src/formats/yml/stringifyEnvironment.ts b/packages/bruno-filestore/src/formats/yml/stringifyEnvironment.ts index 640cef6b2..2f57f4aad 100644 --- a/packages/bruno-filestore/src/formats/yml/stringifyEnvironment.ts +++ b/packages/bruno-filestore/src/formats/yml/stringifyEnvironment.ts @@ -47,7 +47,10 @@ const stringifyEnvironment = (environment: BrunoEnvironment): string => { name: environment.name }; - // Convert variables if they exist + if (environment.color) { + ocEnvironment.color = environment.color; + } + if (environment.variables?.length) { const ocVariables = toOpenCollectionEnvironmentVariables(environment.variables); if (ocVariables) { diff --git a/packages/bruno-filestore/test-results/.last-run.json b/packages/bruno-filestore/test-results/.last-run.json new file mode 100644 index 000000000..5fca3f84b --- /dev/null +++ b/packages/bruno-filestore/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "failed", + "failedTests": [] +} \ No newline at end of file diff --git a/packages/bruno-lang/v2/src/jsonToEnv.js b/packages/bruno-lang/v2/src/jsonToEnv.js index 5fbbf18fd..09811734b 100644 --- a/packages/bruno-lang/v2/src/jsonToEnv.js +++ b/packages/bruno-lang/v2/src/jsonToEnv.js @@ -3,6 +3,8 @@ const { getValueString, indentString } = require('./utils'); const envToJson = (json) => { const variables = _.get(json, 'variables', []); + const color = _.get(json, 'color', null); + const vars = variables .filter((variable) => !variable.secret) .map((variable) => { @@ -20,8 +22,6 @@ const envToJson = (json) => { return indentString(`${prefix}${name}`); }); - const color = _.get(json, 'color', undefined); - let output = ''; if (!variables || !variables.length) { diff --git a/packages/bruno-schema-types/src/collection/environment.ts b/packages/bruno-schema-types/src/collection/environment.ts index 90fdbfaf3..ecc14b7d1 100644 --- a/packages/bruno-schema-types/src/collection/environment.ts +++ b/packages/bruno-schema-types/src/collection/environment.ts @@ -13,6 +13,7 @@ export interface Environment { uid: UID; name: string; variables: EnvironmentVariable[]; + color?: string | null; } export type Environments = Environment[]; diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index 56736cfef..d17fa9f6c 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -17,7 +17,7 @@ const environmentSchema = Yup.object({ uid: uidSchema, name: Yup.string().min(1).required('name is required'), variables: Yup.array().of(environmentVariablesSchema).required('variables are required'), - color: Yup.string().optional() + color: Yup.string().nullable().optional() }) .noUnknown(true) .strict(); diff --git a/tests/environments/color-picker/collection/bruno.json b/tests/environments/color-picker/collection/bruno.json new file mode 100644 index 000000000..a2b575fc0 --- /dev/null +++ b/tests/environments/color-picker/collection/bruno.json @@ -0,0 +1,5 @@ +{ + "version": "1", + "name": "global-env-config-selection", + "type": "collection" +} diff --git a/tests/environments/color-picker/collection/test-request.bru b/tests/environments/color-picker/collection/test-request.bru new file mode 100644 index 000000000..ea3cdd03f --- /dev/null +++ b/tests/environments/color-picker/collection/test-request.bru @@ -0,0 +1,17 @@ +meta { + name: test-request + type: http + seq: 1 +} + +get { + url: {{host}}/api/echo + body: none + auth: none +} + +tests { + test("should get 200 response", function() { + expect(res.getStatus()).to.equal(200); + }); +} diff --git a/tests/environments/color-picker/color-picker.spec.ts b/tests/environments/color-picker/color-picker.spec.ts new file mode 100644 index 000000000..b74b105b3 --- /dev/null +++ b/tests/environments/color-picker/color-picker.spec.ts @@ -0,0 +1,156 @@ +import { test, expect } from '../../../playwright'; +import { closeAllCollections } from '../../utils/page/actions'; + +const PRESET_COLORS = [ + '#CE4F3B', + '#2E8A54', + '#346AB2', + '#C77A0F', + '#B83D7F', + '#8D44B2' +]; + +// Convert hex color to RGB format used by CSS +const hexToRgb = (hex: string): string => { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + if (!result) return ''; + const r = parseInt(result[1], 16); + const g = parseInt(result[2], 16); + const b = parseInt(result[3], 16); + return `rgb(${r}, ${g}, ${b})`; +}; + +test.describe('Color Picker Tests', () => { + test.afterAll(async ({ pageWithUserData: page }) => { + await closeAllCollections(page); + }); + + test('should select a preset color for global environment', async ({ pageWithUserData: page }) => { + // Open the collection from sidebar + await page.locator('#sidebar-collection-name').filter({ hasText: 'global-env-config-selection' }).click(); + + // Open global environment configuration + await page.getByTestId('environment-selector-trigger').click(); + await page.getByTestId('env-tab-global').click(); + await page.getByText('Configure', { exact: true }).click(); + + // Wait for the environments tab to be visible + const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' }); + await expect(envTab).toBeVisible(); + + // Click on the color picker icon (brush icon) next to the environment name + const colorPickerTrigger = page.locator('[title="Change color"]').first(); + await colorPickerTrigger.click(); + + // Wait for the color picker dropdown to appear + const colorPickerDropdown = page.locator('.tippy-box'); + await expect(colorPickerDropdown).toBeVisible(); + + // Select the first preset color (red) using title attribute + const presetColor = PRESET_COLORS[0]; + const colorOption = colorPickerDropdown.locator(`[title="${presetColor}"]`); + await colorOption.click(); + + // Verify the color badge in the environment list shows the selected color + const activeEnvItem = page.locator('.environment-item.active'); + const colorBadge = activeEnvItem.locator('.rounded-full').first(); + await expect(colorBadge).toHaveCSS('background-color', hexToRgb(presetColor)); + }); + + test('should remove color from environment', async ({ pageWithUserData: page }) => { + // Open global environment configuration + await page.getByTestId('environment-selector-trigger').click(); + await page.getByTestId('env-tab-global').click(); + await page.getByText('Configure', { exact: true }).click(); + + // Wait for the environments tab to be visible + const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' }); + await expect(envTab).toBeVisible(); + + // Click on the color picker icon + const colorPickerTrigger = page.locator('[title="Change color"]').first(); + await colorPickerTrigger.click(); + + // Wait for the color picker dropdown to appear + const colorPickerDropdown = page.locator('.tippy-box'); + await expect(colorPickerDropdown).toBeVisible(); + + // Click the "No color" option (ban icon) + const noColorOption = colorPickerDropdown.locator('[title="No color"]'); + await noColorOption.click(); + + // Verify the color badge becomes transparent (no color) + const activeEnvItem = page.locator('.environment-item.active'); + const colorBadge = activeEnvItem.locator('.rounded-full').first(); + await expect(colorBadge).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)'); + }); + + test('should select custom color using slider', async ({ pageWithUserData: page }) => { + // Open global environment configuration + await page.getByTestId('environment-selector-trigger').click(); + await page.getByTestId('env-tab-global').click(); + await page.getByText('Configure', { exact: true }).click(); + + // Wait for the environments tab to be visible + const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' }); + await expect(envTab).toBeVisible(); + + // Click on the color picker icon + const colorPickerTrigger = page.locator('[title="Change color"]').first(); + await colorPickerTrigger.click(); + + // Wait for the color picker dropdown to appear + const colorPickerDropdown = page.locator('.tippy-box'); + await expect(colorPickerDropdown).toBeVisible(); + + // Find the slider and change its value + const slider = colorPickerDropdown.locator('input[type="range"]'); + await expect(slider).toBeVisible(); + + // Move slider to middle position (50%) + await slider.fill('50'); + + // Click the custom color preview to apply it + const customColorPreview = colorPickerDropdown.locator('[title="Custom color"]'); + await customColorPreview.click(); + + // Verify the color badge has a color applied (not transparent) + const activeEnvItem = page.locator('.environment-item.active'); + const colorBadge = activeEnvItem.locator('.rounded-full').first(); + const bgColor = await colorBadge.evaluate((el) => getComputedStyle(el).backgroundColor); + expect(bgColor).not.toBe('rgba(0, 0, 0, 0)'); + expect(bgColor).toMatch(/^rgb\(\d+, \d+, \d+\)$/); + }); + + test('should display color badge in environment list after selecting color', async ({ pageWithUserData: page }) => { + // Open global environment configuration + await page.getByTestId('environment-selector-trigger').click(); + await page.getByTestId('env-tab-global').click(); + await page.getByText('Configure', { exact: true }).click(); + + // Wait for the environments tab to be visible + const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' }); + await expect(envTab).toBeVisible(); + + // Get the currently selected environment name + const activeEnvItem = page.locator('.environment-item.active'); + const envName = await activeEnvItem.locator('.environment-name').textContent(); + + // Click on the color picker icon + const colorPickerTrigger = page.locator('[title="Change color"]').first(); + await colorPickerTrigger.click(); + + // Wait for the color picker dropdown to appear and select a color + const colorPickerDropdown = page.locator('.tippy-box'); + await expect(colorPickerDropdown).toBeVisible(); + + const presetColor = PRESET_COLORS[1]; // green + const colorOption = colorPickerDropdown.locator(`[title="${presetColor}"]`); + await colorOption.click(); + + // Verify the color badge in the environment list shows the selected color + const envListItem = page.locator('.environment-item').filter({ hasText: envName as string }); + const colorBadge = envListItem.locator('.rounded-full').first(); + await expect(colorBadge).toHaveCSS('background-color', hexToRgb(presetColor)); + }); +}); diff --git a/tests/environments/color-picker/init-user-data/collection-security.json b/tests/environments/color-picker/init-user-data/collection-security.json new file mode 100644 index 000000000..bd8bdb1d0 --- /dev/null +++ b/tests/environments/color-picker/init-user-data/collection-security.json @@ -0,0 +1,10 @@ +{ + "collections": [ + { + "path": "{{projectRoot}}/tests/environments/global-env-config-selection/collection", + "securityConfig": { + "jsSandboxMode": "safe" + } + } + ] +} diff --git a/tests/environments/color-picker/init-user-data/global-environments.json b/tests/environments/color-picker/init-user-data/global-environments.json new file mode 100644 index 000000000..45c301067 --- /dev/null +++ b/tests/environments/color-picker/init-user-data/global-environments.json @@ -0,0 +1,47 @@ +{ + "environments": [ + { + "uid": "FlaexlO7lcH7UtEpWsVyz", + "name": "Development Environment", + "variables": [ + { + "uid": "lflBDSYBdHkUedYhBF4Ty", + "name": "env_type", + "value": "development", + "type": "text", + "secret": false, + "enabled": true + } + ] + }, + { + "uid": "MsHcnAIonZ3455OfvpTUT", + "name": "Production Environment", + "variables": [ + { + "uid": "TZljXLErzW1nUWoozntZE", + "name": "env_type", + "value": "production", + "type": "text", + "secret": false, + "enabled": true + } + ] + }, + { + "uid": "VdUAdMPcfapMCqjKAeUiI", + "name": "Staging Environment", + "variables": [ + { + "uid": "FwoWhHvu9eLhA8H4brG6f", + "name": "env_type", + "value": "staging", + "type": "text", + "secret": false, + "enabled": true + } + ] + } + ], + "activeGlobalEnvironmentUid": "MsHcnAIonZ3455OfvpTUT" +} \ No newline at end of file diff --git a/tests/environments/color-picker/init-user-data/preferences.json b/tests/environments/color-picker/init-user-data/preferences.json new file mode 100644 index 000000000..3fa671a0b --- /dev/null +++ b/tests/environments/color-picker/init-user-data/preferences.json @@ -0,0 +1,6 @@ +{ + "maximized": true, + "lastOpenedCollections": [ + "{{projectRoot}}/tests/environments/global-env-config-selection/collection" + ] +}