fix: persist environment color on import/export (#7045)

This commit is contained in:
Pooja
2026-02-11 16:20:39 +05:30
committed by Bijin A B
parent 0ba6c3d132
commit cb3f6629bb
18 changed files with 169 additions and 23 deletions

View File

@@ -46,8 +46,8 @@ const ImportEnvironmentModal = ({ type = 'collection', collection, onClose, onEn
let importedCount = 0;
for (const environment of validEnvironments) {
const action = isGlobal
? addGlobalEnvironment({ name: environment.name, variables: environment.variables })
: importEnvironment({ name: environment.name, variables: environment.variables, collectionUid: collection?.uid });
? addGlobalEnvironment({ name: environment.name, variables: environment.variables, color: environment.color })
: importEnvironment({ name: environment.name, variables: environment.variables, color: environment.color, collectionUid: collection?.uid });
await dispatch(action);
importedCount++;

View File

@@ -1773,7 +1773,7 @@ export const addEnvironment = (name, collectionUid) => (dispatch, getState) => {
});
};
export const importEnvironment = ({ name, variables, collectionUid }) => (dispatch, getState) => {
export const importEnvironment = ({ name, variables, color, collectionUid }) => (dispatch, getState) => {
return new Promise((resolve, reject) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
@@ -1785,7 +1785,7 @@ export const importEnvironment = ({ name, variables, collectionUid }) => (dispat
const { ipcRenderer } = window;
ipcRenderer
.invoke('renderer:create-environment', collection.pathname, sanitizedName, variables)
.invoke('renderer:create-environment', collection.pathname, sanitizedName, variables, color)
.then(
dispatch(
updateLastAction({

View File

@@ -2868,6 +2868,7 @@ export const collectionsSlice = createSlice({
const prevEphemerals = (existingEnv.variables || []).filter((v) => v.ephemeral);
existingEnv.name = environment.name;
existingEnv.variables = environment.variables;
existingEnv.color = environment.color;
/*
Apply temporary (ephemeral) values only to variables that actually exist in the file. This prevents deleted temporaries from “popping back” after a save. If a variable is present in the file, we temporarily override the UI value while also remembering the on-disk value in persistedValue for future saves.
*/

View File

@@ -18,12 +18,13 @@ export const globalEnvironmentsSlice = createSlice({
state.activeGlobalEnvironmentUid = action.payload?.activeGlobalEnvironmentUid;
},
_addGlobalEnvironment: (state, action) => {
const { name, uid, variables = [] } = action.payload;
const { name, uid, variables = [], color } = action.payload;
if (name?.length) {
state.globalEnvironments.push({
uid,
name,
variables
variables,
color
});
}
},
@@ -110,7 +111,7 @@ const getWorkspaceContext = (state) => {
return { workspaceUid, workspacePath: workspace?.pathname };
};
export const addGlobalEnvironment = ({ name, variables = [] }) => (dispatch, getState) => {
export const addGlobalEnvironment = ({ name, variables = [], color }) => (dispatch, getState) => {
return new Promise((resolve, reject) => {
const uid = uuid();
const environment = { name, uid, variables };
@@ -120,12 +121,13 @@ export const addGlobalEnvironment = ({ name, variables = [] }) => (dispatch, get
environmentSchema
.validate(environment)
.then(() => ipcRenderer.invoke('renderer:create-global-environment', { name, uid, variables, workspaceUid, workspacePath }))
.then(() => ipcRenderer.invoke('renderer:create-global-environment', { name, uid, variables, color, workspaceUid, workspacePath }))
.then((result) => {
const finalUid = result?.uid || uid;
const finalName = result?.name || name;
const finalVariables = result?.variables || variables;
dispatch(_addGlobalEnvironment({ name: finalName, uid: finalUid, variables: finalVariables }));
const finalColor = result?.color || color;
dispatch(_addGlobalEnvironment({ name: finalName, uid: finalUid, variables: finalVariables, color: finalColor }));
return finalUid;
})
.then((finalUid) => dispatch(selectGlobalEnvironment({ environmentUid: finalUid })))

View File

@@ -6,7 +6,8 @@ export const exportBrunoEnvironment = async ({ environments, environmentType, fi
let cleanEnvironments = environments.map((environment) => ({
name: environment.name,
variables: (environment.variables || []).map((envVariable) => buildEnvVariable({ envVariable }))
variables: (environment.variables || []).map((envVariable) => buildEnvVariable({ envVariable })),
color: environment.color
}));
await ipcRenderer.invoke('renderer:export-environment', {

View File

@@ -22,7 +22,8 @@ const validateBrunoEnvironment = (env) => {
return {
name: env.name || 'Imported Environment',
variables: env.variables.map((envVariable) => buildEnvVariable({ envVariable, withUuid: true }))
variables: env.variables.map((envVariable) => buildEnvVariable({ envVariable, withUuid: true })),
color: env.color
};
};

View File

@@ -42,7 +42,8 @@ export const fromOpenCollectionEnvironments = (environments: Environment[] | und
enabled: variable.disabled !== true,
secret: isSecret
};
})
}),
color: env.color || null
}));
};
@@ -54,6 +55,7 @@ export const toOpenCollectionEnvironments = (environments: BrunoEnvironment[] |
return environments.map((env): Environment => {
const ocEnv: Environment = {
name: env.name || 'Untitled Environment',
color: env.color ?? undefined,
variables: (env.variables || []).map((v): OCVariable => {
const ocVar: OCVariable = {
name: v.name || '',

View File

@@ -515,7 +515,7 @@ const registerRendererEventHandlers = (mainWindow, watcher) => {
});
// create environment
ipcMain.handle('renderer:create-environment', async (event, collectionPathname, name, variables) => {
ipcMain.handle('renderer:create-environment', async (event, collectionPathname, name, variables, color) => {
try {
const envDirPath = path.join(collectionPathname, 'environments');
if (!fs.existsSync(envDirPath)) {
@@ -538,7 +538,8 @@ const registerRendererEventHandlers = (mainWindow, watcher) => {
const environment = {
name: uniqueName,
variables: variables || []
variables: variables || [],
color
};
if (envHasSecrets(environment)) {
@@ -747,6 +748,7 @@ const registerRendererEventHandlers = (mainWindow, watcher) => {
const environmentWithInfo = (environment) => ({
name: environment.name,
variables: environment.variables,
color: environment.color,
info: {
type: 'bruno-environment',
exportedAt: new Date().toISOString(),

View File

@@ -6,7 +6,7 @@ const { globalEnvironmentsStore } = require('../store/global-environments');
const { generateUniqueName, sanitizeName, writeFile, isValidDotEnvFilename } = require('../utils/filesystem');
const registerGlobalEnvironmentsIpc = (mainWindow, workspaceEnvironmentsManager) => {
ipcMain.handle('renderer:create-global-environment', async (event, { uid, name, variables, workspaceUid, workspacePath }) => {
ipcMain.handle('renderer:create-global-environment', async (event, { uid, name, variables, color, workspaceUid, workspacePath }) => {
try {
// If workspace path provided, use workspace environments manager
if (workspacePath && workspaceEnvironmentsManager) {
@@ -16,7 +16,7 @@ const registerGlobalEnvironmentsIpc = (mainWindow, workspaceEnvironmentsManager)
const sanitizedName = sanitizeName(name);
const uniqueName = generateUniqueName(sanitizedName, (name) => existingNames.includes(name));
return await workspaceEnvironmentsManager.addGlobalEnvironmentByPath(workspacePath, { uid, name: uniqueName, variables });
return await workspaceEnvironmentsManager.addGlobalEnvironmentByPath(workspacePath, { uid, name: uniqueName, variables, color });
}
const existingGlobalEnvironments = globalEnvironmentsStore.getGlobalEnvironments();
@@ -25,9 +25,9 @@ const registerGlobalEnvironmentsIpc = (mainWindow, workspaceEnvironmentsManager)
const sanitizedName = sanitizeName(name);
const uniqueName = generateUniqueName(sanitizedName, (name) => existingNames.includes(name));
globalEnvironmentsStore.addGlobalEnvironment({ uid, name: uniqueName, variables });
globalEnvironmentsStore.addGlobalEnvironment({ uid, name: uniqueName, variables, color });
return { name: uniqueName };
return { name: uniqueName, color };
} catch (error) {
console.error('Error in renderer:create-global-environment:', error);
return Promise.reject(error);

View File

@@ -97,7 +97,7 @@ class GlobalEnvironmentsStore {
return this.store.set('activeGlobalEnvironmentUid', uid);
}
addGlobalEnvironment({ uid, name, variables = [] }) {
addGlobalEnvironment({ uid, name, variables = [], color }) {
let globalEnvironments = this.getGlobalEnvironments();
const existingEnvironment = globalEnvironments.find((env) => env?.name == name);
if (existingEnvironment) {
@@ -106,7 +106,8 @@ class GlobalEnvironmentsStore {
globalEnvironments.push({
uid,
name,
variables
variables,
color
});
this.setGlobalEnvironments(globalEnvironments);
}

View File

@@ -171,7 +171,7 @@ class GlobalEnvironmentsManager {
});
}
async createGlobalEnvironment(workspacePath, { uid, name, variables }) {
async createGlobalEnvironment(workspacePath, { uid, name, variables, color }) {
try {
if (!workspacePath) {
throw new Error('Workspace path is required');
@@ -191,7 +191,8 @@ class GlobalEnvironmentsManager {
const environment = {
name: name,
variables: variables || []
variables: variables || [],
color
};
if (this.envHasSecrets(environment)) {
@@ -204,7 +205,8 @@ class GlobalEnvironmentsManager {
return {
uid: generateUidBasedOnHash(environmentFilePath),
name,
variables
variables,
color
};
} catch (error) {
throw error;

View File

@@ -0,0 +1,58 @@
import { test, expect } from '../../../../playwright';
import path from 'path';
import { closeAllCollections } from '../../../utils/page';
test.describe.serial('Environment Color Import Tests', () => {
test.afterAll(async ({ pageWithUserData: page }) => {
await closeAllCollections(page);
});
test('should import global environment with color preserved', async ({ pageWithUserData: page }) => {
const envWithColorFile = path.join(__dirname, 'fixtures/env-with-color.json');
await test.step('Open collection and navigate to global environment import', async () => {
// Open the collection from sidebar
const collectionName = page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Color Import Test Collection' });
await expect(collectionName).toBeVisible();
await collectionName.click();
// Open environment selector dropdown
const envSelector = page.getByTestId('environment-selector-trigger');
await expect(envSelector).toBeVisible();
await envSelector.click();
// Click global tab
const globalTab = page.getByTestId('env-tab-global');
await expect(globalTab).toBeVisible();
await globalTab.click();
// Verify global tab is active
await expect(globalTab).toHaveClass(/active/);
// Click Import button
await page.getByRole('button', { name: 'Import', exact: true }).click();
// Verify import modal opens
const importModal = page.getByTestId('import-global-environment-modal');
await expect(importModal).toBeVisible();
});
await test.step('Import environment with color', async () => {
// Import environment file
const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByTestId('import-global-environment').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(envWithColorFile);
// Wait for the environment tab to appear
const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });
await expect(envTab).toBeVisible();
});
await test.step('Verify imported environment has the color badge displayed', async () => {
// The color badge should be visible in the environment details
const colorBadge = page.locator('div.rounded-full[style*="background-color: rgb(16, 185, 129)"]').first();
await expect(colorBadge).toBeVisible();
});
});
});

View File

@@ -0,0 +1,5 @@
{
"version": "1",
"name": "Environment Color Import Test Collection",
"type": "collection"
}

View File

@@ -0,0 +1,11 @@
meta {
name: Test Request
type: http
seq: 1
}
get {
url: https://httpbin.org/get
body: none
auth: none
}

View File

@@ -0,0 +1,18 @@
{
"name": "colored-env",
"variables": [
{
"name": "baseUrl",
"value": "https://api.example.com",
"type": "text",
"enabled": true,
"secret": false
}
],
"color": "#10B981",
"info": {
"type": "bruno-environment",
"exportedAt": "2024-01-01T00:00:00.000Z",
"exportedUsing": "Bruno/v1.0.0"
}
}

View File

@@ -0,0 +1,35 @@
{
"info": {
"type": "bruno-environment",
"exportedAt": "2024-01-01T00:00:00.000Z",
"exportedUsing": "Bruno/v1.0.0"
},
"environments": [
{
"name": "dev",
"variables": [
{
"name": "apiUrl",
"value": "https://dev.api.example.com",
"type": "text",
"enabled": true,
"secret": false
}
],
"color": "#3B82F6"
},
{
"name": "staging",
"variables": [
{
"name": "apiUrl",
"value": "https://staging.api.example.com",
"type": "text",
"enabled": true,
"secret": false
}
],
"color": "#F59E0B"
}
]
}

View File

@@ -0,0 +1,6 @@
{
"maximized": false,
"lastOpenedCollections": [
"{{projectRoot}}/tests/environments/import-environment/env-color-import/fixtures/collection"
]
}