mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-15 20:01:28 +00:00
fix: persist environment color on import/export (#7045)
This commit is contained in:
@@ -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++;
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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 })))
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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 || '',
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "Environment Color Import Test Collection",
|
||||
"type": "collection"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: Test Request
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: https://httpbin.org/get
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"maximized": false,
|
||||
"lastOpenedCollections": [
|
||||
"{{projectRoot}}/tests/environments/import-environment/env-color-import/fixtures/collection"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user