feat(#304) Environments color 🎨 (#1053) (#6974)

This commit is contained in:
Pooja
2026-01-30 16:23:51 +05:30
committed by Sid
parent 5a6714f085
commit a04ff3e819
38 changed files with 908 additions and 753 deletions

439
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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 (
<div
className="flex-shrink-0 rounded-full"
style={{
width: sizeValue,
height: sizeValue,
backgroundColor: color || 'transparent',
border: showBorder ? '1px solid' : 'none',
borderColor: showBorder ? theme.background.surface1 : 'transparent'
}}
/>
);
};
export default ColorBadge;

View File

@@ -0,0 +1,7 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
`;
export default StyledWrapper;

View File

@@ -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 <ColorBadge color={color} size={8} />;
}
return <IconBrush size={14} strokeWidth={1.5} className="opacity-70" />;
};
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 = (
<div className="cursor-pointer flex items-center" title="Change color">
<ColorPickerIcon color={color} />
</div>
);
const colorPickerContent = (
<StyledWrapper>
<div className="p-2">
<div className="flex flex-wrap gap-1.5 justify-between items-center">
<div
className="w-5 h-5 cursor-pointer flex items-center justify-center transition-transform duration-100 hover:scale-110"
onClick={() => handleColorSelect(null)}
title="No color"
>
<IconBan size={20} strokeWidth={1.5} />
</div>
{PRESET_COLORS.map((presetColor, index) => (
<div
key={index}
className={`w-5 h-5 rounded cursor-pointer flex items-center justify-center transition-transform duration-100 hover:scale-110 border-2 border-transparent
${color === presetColor ? 'border-solid !border-current' : ''}
`}
style={{ backgroundColor: presetColor }}
onClick={() => handleColorSelect(presetColor)}
title={presetColor}
/>
))}
</div>
<div className="flex items-center gap-2 mt-2 pt-2">
<div
className="w-4 h-4 rounded-full flex-shrink-0 cursor-pointer"
style={{ backgroundColor: customColor }}
onClick={() => handleColorSelect(customColor)}
title="Custom color"
/>
<ColorRangePicker
className="flex-1"
value={sliderPosition}
onChange={handleSliderChange}
onMouseUp={handleSliderEnd}
selectedColor={customColor}
colorRange={COLOR_RANGE_SEQUENCE}
/>
</div>
</div>
</StyledWrapper>
);
return (
<Dropdown icon={icon || defaultIcon} placement="bottom-start">
{colorPickerContent}
</Dropdown>
);
};
export default ColorPicker;

View File

@@ -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;

View File

@@ -0,0 +1,23 @@
import StyledWrapper from './StyledWrapper';
const ColorRangePicker = ({ selectedColor, className, value, onChange, colorRange, ...props }) => {
return (
<StyledWrapper color={selectedColor}>
<input
type="range"
min="0"
max="100"
value={value}
onChange={onChange}
className={`hue-slider ${className}`}
style={{
background: `linear-gradient(to right, ${colorRange.join(',')})`
}}
title="Adjust color"
{...props}
/>
</StyledWrapper>
);
};
export default ColorRangePicker;

View File

@@ -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}
>
<IconDatabase size={16} strokeWidth={1.5} color={env.color} />
<ColorBadge color={env.color} size={8} showEmptyBorder={false} />
<span className="max-w-100% truncate no-wrap">{env.name}</span>
</div>
))}

View File

@@ -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;

View File

@@ -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: <IconDatabase size={16} strokeWidth={1.5} /> },
{ id: 'global', label: 'Global', icon: <IconWorld size={16} strokeWidth={1.5} /> }
];
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 (
<>
<Icon size={14} strokeWidth={1.5} className="env-icon" style={colorStyle} />
<ToolHint
text={environment.name}
toolhintId={`env-${environment.uid}`}
place="bottom-start"
delayShow={1000}
hidden={environment.name?.length < 7}
>
<span className="env-text max-w-24 truncate overflow-hidden" style={colorStyle}>
{environment.name}
</span>
</ToolHint>
</>
);
};
/**
* Dropdown trigger component showing active environments
*/
const DropdownTrigger = forwardRef(({ collectionEnv, globalEnv }, ref) => {
const hasAnyEnv = collectionEnv || globalEnv;
// Empty state - no environments selected
if (!hasAnyEnv) {
return (
<div
ref={ref}
className="current-environment flex align-center justify-center cursor-pointer bg-transparent no-environments"
data-testid="environment-selector-trigger"
>
<span className="env-text-inactive max-w-36 truncate no-wrap">No Environment</span>
<IconCaretDown className="caret flex items-center justify-center" size={12} strokeWidth={2} />
</div>
);
}
// Only collection env selected - caret goes with collection env
if (collectionEnv && !globalEnv) {
return (
<div
ref={ref}
className="current-environment flex align-center justify-center cursor-pointer bg-transparent"
style={{ padding: 0 }}
data-testid="environment-selector-trigger"
>
<div className="flex items-center" style={getEnvBadgeStyle(collectionEnv, 'left', false)}>
<EnvironmentBadge environment={collectionEnv} icon={IconDatabase} />
<IconCaretDown className="caret flex items-center justify-center" size={12} strokeWidth={2} />
</div>
</div>
);
}
// Only global env selected - caret goes with global env
if (!collectionEnv && globalEnv) {
return (
<div
ref={ref}
className="current-environment flex align-center justify-center cursor-pointer bg-transparent"
style={{ padding: 0 }}
data-testid="environment-selector-trigger"
>
<div className="flex items-center" style={getEnvBadgeStyle(globalEnv, 'right', false)}>
<EnvironmentBadge environment={globalEnv} icon={IconWorld} />
<IconCaretDown className="caret flex items-center justify-center" size={12} strokeWidth={2} />
</div>
</div>
);
}
// Both environments selected
return (
<div
ref={ref}
className="current-environment flex align-center justify-center cursor-pointer bg-transparent"
style={{ padding: 0 }}
data-testid="environment-selector-trigger"
>
{/* Collection Environment Section */}
<div className="flex items-center" style={getEnvBadgeStyle(collectionEnv, 'left', true)}>
<EnvironmentBadge environment={collectionEnv} icon={IconDatabase} />
</div>
{/* Separator */}
<div className="env-separator" style={{ width: '1px', alignSelf: 'stretch' }} />
{/* Global Environment Section + Caret */}
<div className="flex items-center" style={getEnvBadgeStyle(globalEnv, 'right', true)}>
<EnvironmentBadge environment={globalEnv} icon={IconWorld} />
<IconCaretDown className="caret flex items-center justify-center" size={12} strokeWidth={2} />
</div>
</div>
);
});
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: <IconDatabase size={16} strokeWidth={1.5} /> },
{ id: 'global', label: 'Global', icon: <IconWorld size={16} strokeWidth={1.5} /> }
];
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 && (
<>
<div className="flex items-center">
<ToolHint
className="active-env-toolhint"
text={activeCollectionEnvironment.name}
toolhintId={`collection-env-${activeCollectionEnvironment.uid}`}
place="bottom-start"
delayShow={1000}
hidden={activeCollectionEnvironment.name?.length < 7}
>
<IconDatabase size={14} strokeWidth={1.5} className="env-icon" />
<span className="env-text max-w-24 truncate overflow-hidden">{activeCollectionEnvironment.name}</span>
</ToolHint>
</div>
{activeGlobalEnvironment && <span className="env-separator">|</span>}
</>
)}
{activeGlobalEnvironment && (
<div className="flex items-center">
<IconWorld size={14} strokeWidth={1.5} className="env-icon" />
<ToolHint
text={activeGlobalEnvironment.name}
toolhintId={`global-env-${activeGlobalEnvironment.uid}`}
place="bottom-start"
delayShow={1000}
hidden={activeGlobalEnvironment.name?.length < 7}
>
<span className="env-text max-w-24 truncate overflow-hidden">{activeGlobalEnvironment.name}</span>
</ToolHint>
</div>
)}
</>
) : (
<span className="env-text-inactive max-w-36 truncate no-wrap">No Environment</span>
const openEnvironmentSettingsTab = (type) => {
dispatch(
addTab({
uid: `${collection.uid}-${type}-settings`,
collectionUid: collection.uid,
type: `${type}-settings`
})
);
return (
<div
ref={ref}
className={`current-environment flex align-center justify-center cursor-pointer bg-transparent ${
!hasAnyEnv ? 'no-environments' : ''
}`}
data-testid="environment-selector-trigger"
>
{displayContent}
<IconCaretDown className="caret flex items-center justify-center" size={12} strokeWidth={2} />
</div>
);
});
};
return (
<StyledWrapper color={activeCollectionEnvironment?.color} width={dropdownWidth}>
<StyledWrapper width={dropdownWidth}>
<div className="environment-selector flex align-center cursor-pointer">
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
<Dropdown
onCreate={(ref) => (dropdownTippyRef.current = ref)}
icon={<DropdownTrigger collectionEnv={activeCollectionEnvironment} globalEnv={activeGlobalEnvironment} />}
placement="bottom-end"
>
{/* Tab Headers */}
<div className="tab-header flex pt-3 pb-2 px-3">
{tabs.map((tab) => (
{TABS.map((tab) => (
<button
key={tab.id}
className={`tab-button whitespace-nowrap pb-[0.375rem] border-b-[0.125rem] bg-transparent flex align-center cursor-pointer transition-all duration-200 mr-[1.25rem] ${
@@ -223,15 +305,7 @@ const EnvironmentSelector = ({ collection }) => {
{showCreateGlobalModal && (
<CreateGlobalEnvironment
onClose={() => setShowCreateGlobalModal(false)}
onEnvironmentCreated={() => {
dispatch(
addTab({
uid: `${collection.uid}-global-environment-settings`,
collectionUid: collection.uid,
type: 'global-environment-settings'
})
);
}}
onEnvironmentCreated={() => openEnvironmentSettingsTab('global-environment')}
/>
)}
@@ -239,15 +313,7 @@ const EnvironmentSelector = ({ collection }) => {
<ImportEnvironmentModal
type="global"
onClose={() => setShowImportGlobalModal(false)}
onEnvironmentCreated={() => {
dispatch(
addTab({
uid: `${collection.uid}-global-environment-settings`,
collectionUid: collection.uid,
type: 'global-environment-settings'
})
);
}}
onEnvironmentCreated={() => openEnvironmentSettingsTab('global-environment')}
/>
)}
@@ -255,15 +321,7 @@ const EnvironmentSelector = ({ collection }) => {
<CreateEnvironment
collection={collection}
onClose={() => setShowCreateCollectionModal(false)}
onEnvironmentCreated={() => {
dispatch(
addTab({
uid: `${collection.uid}-environment-settings`,
collectionUid: collection.uid,
type: 'environment-settings'
})
);
}}
onEnvironmentCreated={() => openEnvironmentSettingsTab('environment')}
/>
)}
@@ -272,15 +330,7 @@ const EnvironmentSelector = ({ collection }) => {
type="collection"
collection={collection}
onClose={() => setShowImportCollectionModal(false)}
onEnvironmentCreated={() => {
dispatch(
addTab({
uid: `${collection.uid}-environment-settings`,
collectionUid: collection.uid,
type: 'environment-settings'
})
);
}}
onEnvironmentCreated={() => openEnvironmentSettingsTab('environment')}
/>
)}
</StyledWrapper>

View File

@@ -1,32 +0,0 @@
import React from 'react';
import { useCallback } from 'react';
import toast from 'react-hot-toast';
import { useDispatch } from 'react-redux';
import { saveEnvironmentColor } from 'providers/ReduxStore/slices/collections/actions';
import { Circle } from '@uiw/react-color';
const EnvironmentColor = ({ environment, collectionUid }) => {
const dispatch = useDispatch();
const onColorChange = useCallback(
(color) => {
if (color == environment.color) return;
dispatch(saveEnvironmentColor(color, environment.uid, collectionUid))
.then(() => toast.success('Environment color changed successfully'))
.catch(() => toast.error('An error occurred while changing the environment color'));
},
[dispatch, environment.uid, environment.color, collectionUid]
);
return (
<Circle
id="environment-color"
style={{ gap: 3 }}
pointProps={{ style: { width: 14, height: 14, borderRadius: 10 } }}
colors={['#000000','#9c27b0','#3f51b5','#03a9f4','#009688','#8bc34a','#ffeb3b','#ff9800','#ff5722','#795548','#607d8b']}
color={environment.color}
onChange={(color) => onColorChange(color.hex)}
/>
);
};
export default EnvironmentColor;

View File

@@ -1,14 +1,13 @@
import { IconCopy, IconEdit, IconTrash, IconCheck, IconX } from '@tabler/icons';
import { useState, useRef } from 'react';
import { useDispatch } from 'react-redux';
import { renameEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import { renameEnvironment, updateEnvironmentColor } from 'providers/ReduxStore/slices/collections/actions';
import { validateName, validateNameError } from 'utils/common/regex';
import toast from 'react-hot-toast';
import CopyEnvironment from 'components/Environments/EnvironmentSettings/CopyEnvironment';
import DeleteEnvironment from 'components/Environments/EnvironmentSettings/DeleteEnvironment';
import EnvironmentVariables from './EnvironmentVariables';
import EnvironmentColor from '../EnvironmentDetails/EnvironmentColor';
import ToolHint from 'components/ToolHint/index';
import ColorPicker from 'components/ColorPicker';
import StyledWrapper from './StyledWrapper';
const EnvironmentDetails = ({ environment, setIsModified, collection }) => {
@@ -113,6 +112,16 @@ const EnvironmentDetails = ({ environment, setIsModified, collection }) => {
}
};
const handleColorChange = (color) => {
dispatch(updateEnvironmentColor(environment.uid, color, collection.uid))
.then(() => {
toast.success('Environment color updated!');
})
.catch(() => {
toast.error('An error occurred while updating the environment color');
});
};
return (
<StyledWrapper>
{openDeleteModal && (
@@ -121,6 +130,7 @@ const EnvironmentDetails = ({ environment, setIsModified, collection }) => {
{openCopyModal && (
<CopyEnvironment onClose={() => setOpenCopyModal(false)} environment={environment} collection={collection} />
)}
<div className="header">
<div className={`title-container ${isRenaming ? 'renaming' : ''}`}>
{isRenaming ? (
@@ -158,7 +168,10 @@ const EnvironmentDetails = ({ environment, setIsModified, collection }) => {
</div>
</>
) : (
<h2 className="title">{environment.name}</h2>
<div className="flex items-center gap-2">
<h2 className="title">{environment.name}</h2>
<ColorPicker color={environment.color} onChange={handleColorChange} />
</div>
)}
</div>
{nameError && isRenaming && <div className="title-error">{nameError}</div>}
@@ -175,8 +188,9 @@ const EnvironmentDetails = ({ environment, setIsModified, collection }) => {
</div>
</div>
<EnvironmentColor environment={environment} collectionUid={collection.uid} />
<EnvironmentVariables environment={environment} collection={collection} setIsModified={setIsModified} onClose={onClose} />
<div className="content">
<EnvironmentVariables environment={environment} setIsModified={setIsModified} collection={collection} />
</div>
</StyledWrapper>
);
};

View File

@@ -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;

View File

@@ -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
</div>
) : (
<>
<ColorBadge color={env.color} size={8} />
<span className="environment-name">{env.name}</span>
<div className="environment-actions">
{activeEnvironmentUid === env.uid ? (

View File

@@ -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;

View File

@@ -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 <StyledWrapper>Something went wrong!</StyledWrapper>;
}
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 (
<StyledWrapper color={activeEnvironment?.color} className={getRootClassname()}>
<StyledWrapper>
{newRequestModalOpen && (
<NewRequest collectionUid={activeCollection?.uid} onClose={() => setNewRequestModalOpen(false)} />
)}

View File

@@ -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 (
<StyledWrapper>
{openDeleteModal && (
@@ -159,7 +170,10 @@ const EnvironmentDetails = ({ environment, setIsModified, collection }) => {
</div>
</>
) : (
<h2 className="title">{environment.name}</h2>
<div className="flex items-center gap-2">
<h2 className="title">{environment.name}</h2>
<ColorPicker color={environment.color} onChange={handleColorChange} />
</div>
)}
</div>
{nameError && isRenaming && <div className="title-error">{nameError}</div>}

View File

@@ -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;

View File

@@ -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
</div>
) : (
<>
<ColorBadge color={env.color} size={8} />
<span className="environment-name">{env.name}</span>
<div className="environment-actions">
{activeEnvironmentUid === env.uid ? (

View File

@@ -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();

View File

@@ -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,

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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();

View File

@@ -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();

View File

@@ -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;

View File

@@ -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) {

View File

@@ -0,0 +1,4 @@
{
"status": "failed",
"failedTests": []
}

View File

@@ -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) {

View File

@@ -13,6 +13,7 @@ export interface Environment {
uid: UID;
name: string;
variables: EnvironmentVariable[];
color?: string | null;
}
export type Environments = Environment[];

View File

@@ -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();

View File

@@ -0,0 +1,5 @@
{
"version": "1",
"name": "global-env-config-selection",
"type": "collection"
}

View File

@@ -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);
});
}

View File

@@ -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));
});
});

View File

@@ -0,0 +1,10 @@
{
"collections": [
{
"path": "{{projectRoot}}/tests/environments/global-env-config-selection/collection",
"securityConfig": {
"jsSandboxMode": "safe"
}
}
]
}

View File

@@ -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"
}

View File

@@ -0,0 +1,6 @@
{
"maximized": true,
"lastOpenedCollections": [
"{{projectRoot}}/tests/environments/global-env-config-selection/collection"
]
}