From cb3f6629bb203eda306d991c8b1bcc2800e15651 Mon Sep 17 00:00:00 2001 From: Pooja Date: Wed, 11 Feb 2026 16:20:39 +0530 Subject: [PATCH] fix: persist environment color on import/export (#7045) --- .../Common/ImportEnvironmentModal/index.js | 4 +- .../ReduxStore/slices/collections/actions.js | 4 +- .../ReduxStore/slices/collections/index.js | 1 + .../ReduxStore/slices/global-environments.js | 12 ++-- .../src/utils/exporters/bruno-environment.js | 3 +- .../src/utils/importers/bruno-environment.js | 3 +- .../src/opencollection/environment.ts | 4 +- packages/bruno-electron/src/ipc/collection.js | 6 +- .../src/ipc/global-environments.js | 8 +-- .../src/store/global-environments.js | 5 +- .../src/store/workspace-environments.js | 8 ++- .../env-color-import/env-color-import.spec.ts | 58 +++++++++++++++++++ .../fixtures/collection/bruno.json | 5 ++ .../fixtures/collection/test-request.bru | 11 ++++ .../fixtures/env-with-color.json | 18 ++++++ .../fixtures/multiple-envs-with-colors.json | 35 +++++++++++ .../init-user-data/collection-security.json | 1 + .../init-user-data/preferences.json | 6 ++ 18 files changed, 169 insertions(+), 23 deletions(-) create mode 100644 tests/environments/import-environment/env-color-import/env-color-import.spec.ts create mode 100644 tests/environments/import-environment/env-color-import/fixtures/collection/bruno.json create mode 100644 tests/environments/import-environment/env-color-import/fixtures/collection/test-request.bru create mode 100644 tests/environments/import-environment/env-color-import/fixtures/env-with-color.json create mode 100644 tests/environments/import-environment/env-color-import/fixtures/multiple-envs-with-colors.json create mode 100644 tests/environments/import-environment/env-color-import/init-user-data/collection-security.json create mode 100644 tests/environments/import-environment/env-color-import/init-user-data/preferences.json diff --git a/packages/bruno-app/src/components/Environments/Common/ImportEnvironmentModal/index.js b/packages/bruno-app/src/components/Environments/Common/ImportEnvironmentModal/index.js index b14f536a7..a1ff3f1d6 100644 --- a/packages/bruno-app/src/components/Environments/Common/ImportEnvironmentModal/index.js +++ b/packages/bruno-app/src/components/Environments/Common/ImportEnvironmentModal/index.js @@ -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++; 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 2aa6144ef..f59af7ddc 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -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({ 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 9972b7c7d..c11bd6b93 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -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. */ 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 5eb823ada..dfd4830b8 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js @@ -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 }))) diff --git a/packages/bruno-app/src/utils/exporters/bruno-environment.js b/packages/bruno-app/src/utils/exporters/bruno-environment.js index dc3d0a0f8..6b8fcc723 100644 --- a/packages/bruno-app/src/utils/exporters/bruno-environment.js +++ b/packages/bruno-app/src/utils/exporters/bruno-environment.js @@ -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', { diff --git a/packages/bruno-app/src/utils/importers/bruno-environment.js b/packages/bruno-app/src/utils/importers/bruno-environment.js index 7cf2f1e27..db1955a03 100644 --- a/packages/bruno-app/src/utils/importers/bruno-environment.js +++ b/packages/bruno-app/src/utils/importers/bruno-environment.js @@ -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 }; }; diff --git a/packages/bruno-converters/src/opencollection/environment.ts b/packages/bruno-converters/src/opencollection/environment.ts index 05d4c0d16..4b5552b45 100644 --- a/packages/bruno-converters/src/opencollection/environment.ts +++ b/packages/bruno-converters/src/opencollection/environment.ts @@ -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 || '', diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index a2df15e6a..5b95d7823 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -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(), diff --git a/packages/bruno-electron/src/ipc/global-environments.js b/packages/bruno-electron/src/ipc/global-environments.js index 7821e233e..91ecdddab 100644 --- a/packages/bruno-electron/src/ipc/global-environments.js +++ b/packages/bruno-electron/src/ipc/global-environments.js @@ -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); diff --git a/packages/bruno-electron/src/store/global-environments.js b/packages/bruno-electron/src/store/global-environments.js index 084803af3..e5c7b484e 100644 --- a/packages/bruno-electron/src/store/global-environments.js +++ b/packages/bruno-electron/src/store/global-environments.js @@ -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); } diff --git a/packages/bruno-electron/src/store/workspace-environments.js b/packages/bruno-electron/src/store/workspace-environments.js index 3098f2b35..f10c3d2b2 100644 --- a/packages/bruno-electron/src/store/workspace-environments.js +++ b/packages/bruno-electron/src/store/workspace-environments.js @@ -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; diff --git a/tests/environments/import-environment/env-color-import/env-color-import.spec.ts b/tests/environments/import-environment/env-color-import/env-color-import.spec.ts new file mode 100644 index 000000000..23d64e1c2 --- /dev/null +++ b/tests/environments/import-environment/env-color-import/env-color-import.spec.ts @@ -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(); + }); + }); +}); diff --git a/tests/environments/import-environment/env-color-import/fixtures/collection/bruno.json b/tests/environments/import-environment/env-color-import/fixtures/collection/bruno.json new file mode 100644 index 000000000..1fc3578bd --- /dev/null +++ b/tests/environments/import-environment/env-color-import/fixtures/collection/bruno.json @@ -0,0 +1,5 @@ +{ + "version": "1", + "name": "Environment Color Import Test Collection", + "type": "collection" +} diff --git a/tests/environments/import-environment/env-color-import/fixtures/collection/test-request.bru b/tests/environments/import-environment/env-color-import/fixtures/collection/test-request.bru new file mode 100644 index 000000000..795bb76e4 --- /dev/null +++ b/tests/environments/import-environment/env-color-import/fixtures/collection/test-request.bru @@ -0,0 +1,11 @@ +meta { + name: Test Request + type: http + seq: 1 +} + +get { + url: https://httpbin.org/get + body: none + auth: none +} diff --git a/tests/environments/import-environment/env-color-import/fixtures/env-with-color.json b/tests/environments/import-environment/env-color-import/fixtures/env-with-color.json new file mode 100644 index 000000000..3b499b599 --- /dev/null +++ b/tests/environments/import-environment/env-color-import/fixtures/env-with-color.json @@ -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" + } +} diff --git a/tests/environments/import-environment/env-color-import/fixtures/multiple-envs-with-colors.json b/tests/environments/import-environment/env-color-import/fixtures/multiple-envs-with-colors.json new file mode 100644 index 000000000..34944f8de --- /dev/null +++ b/tests/environments/import-environment/env-color-import/fixtures/multiple-envs-with-colors.json @@ -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" + } + ] +} diff --git a/tests/environments/import-environment/env-color-import/init-user-data/collection-security.json b/tests/environments/import-environment/env-color-import/init-user-data/collection-security.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/tests/environments/import-environment/env-color-import/init-user-data/collection-security.json @@ -0,0 +1 @@ +{} diff --git a/tests/environments/import-environment/env-color-import/init-user-data/preferences.json b/tests/environments/import-environment/env-color-import/init-user-data/preferences.json new file mode 100644 index 000000000..f3316f8cc --- /dev/null +++ b/tests/environments/import-environment/env-color-import/init-user-data/preferences.json @@ -0,0 +1,6 @@ +{ + "maximized": false, + "lastOpenedCollections": [ + "{{projectRoot}}/tests/environments/import-environment/env-color-import/fixtures/collection" + ] +}