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 (
+
+
+
+
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 0c74c6d9b..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 } from '@tabler/icons';
import ToolHint from 'components/ToolHint';
+import ColorBadge from 'components/ColorBadge';
const EnvironmentListContent = ({
environments,
@@ -38,6 +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 d36b5a8ee..082c7a50f 100644
--- a/packages/bruno-app/src/components/Environments/EnvironmentSelector/StyledWrapper.js
+++ b/packages/bruno-app/src/components/Environments/EnvironmentSelector/StyledWrapper.js
@@ -33,8 +33,7 @@ const Wrapper = styled.div`
}
.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 {
diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js
index 8b454afb5..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 (
+ <>
+
+
+
+ {environment.name}
+
+
+ >
+ );
+};
+
+/**
+ * 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,159 +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 && (
- <>
-
-
-
- {activeCollectionEnvironment.name}
-
-
- {activeGlobalEnvironment &&
|}
- >
- )}
- {activeGlobalEnvironment && (
-
-
-
- {activeGlobalEnvironment.name}
-
-
- )}
- >
- ) : (
-
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) => (
>
) : (
- {environment.name}
+
+
{environment.name}
+
+
)}
{nameError && isRenaming && {nameError}
}
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 e7d154022..c7f40a13f 100644
--- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/index.js
+++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/index.js
@@ -6,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';
@@ -367,6 +368,7 @@ const EnvironmentList = ({
) : (
<>
+
{env.name}
{activeEnvironmentUid === env.uid ? (
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 aea5c0352..f3f3cfef0 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
@@ -49,6 +49,7 @@ import {
updateActiveConnections,
saveRequest as _saveRequest,
saveEnvironment as _saveEnvironment,
+ updateEnvironmentColor as _updateEnvironmentColor,
saveCollectionDraft,
saveFolderDraft,
addVar,
@@ -1930,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
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 753d50124..efec9f44a 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
@@ -277,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);
@@ -3465,6 +3477,7 @@ export const {
collectionUnlinkEnvFileEvent,
saveEnvironment,
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/envToJson.js b/packages/bruno-lang/v2/src/envToJson.js
index 124d22318..a4e97a941 100644
--- a/packages/bruno-lang/v2/src/envToJson.js
+++ b/packages/bruno-lang/v2/src/envToJson.js
@@ -12,7 +12,7 @@ const _ = require('lodash');
// }
const indentLevel = 4;
const grammar = ohm.grammar(`Bru {
- BruEnvFile = (vars | secretvars)*
+ BruEnvFile = (vars | secretvars | color)*
nl = "\\r"? "\\n"
st = " " | "\\t"
@@ -43,6 +43,7 @@ const grammar = ohm.grammar(`Bru {
secretvars = "vars:secret" array
vars = "vars" dictionary
+ color = "color:" any*
}`);
const mapPairListToKeyValPairs = (pairList = []) => {
@@ -190,6 +191,11 @@ const sem = grammar.createSemantics().addAttribute('ast', {
return {
variables: vars
};
+ },
+ color: (_1, anystring) => {
+ return {
+ color: anystring.sourceString.trim()
+ };
}
});
diff --git a/packages/bruno-lang/v2/src/jsonToEnv.js b/packages/bruno-lang/v2/src/jsonToEnv.js
index 185dc46bb..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,13 +22,14 @@ const envToJson = (json) => {
return indentString(`${prefix}${name}`);
});
+ let output = '';
+
if (!variables || !variables.length) {
- return `vars {
+ output += `vars {
}
`;
}
- let output = '';
if (vars.length) {
output += `vars {
${vars.join('\n')}
@@ -38,6 +41,10 @@ ${vars.join('\n')}
output += `vars:secret [
${secretVars.join(',\n')}
]
+`;
+ }
+ if (color) {
+ output += `color: ${color}
`;
}
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 09b40e1ed..d17fa9f6c 100644
--- a/packages/bruno-schema/src/collections/index.js
+++ b/packages/bruno-schema/src/collections/index.js
@@ -16,7 +16,8 @@ const environmentVariablesSchema = Yup.object({
const environmentSchema = Yup.object({
uid: uidSchema,
name: Yup.string().min(1).required('name is required'),
- variables: Yup.array().of(environmentVariablesSchema).required('variables are required')
+ variables: Yup.array().of(environmentVariablesSchema).required('variables are required'),
+ 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"
+ ]
+}