Merge pull request #6069 from pooja-bruno/feat/add-edit-variable-in-place

feat: edit variable in place
This commit is contained in:
Pooja
2025-11-17 16:13:09 +05:30
committed by GitHub
parent 27a7b623c7
commit 4631eda281
16 changed files with 1605 additions and 118 deletions

View File

@@ -69,6 +69,7 @@ import { buildPersistedEnvVariables } from 'utils/environments';
import { safeParseJSON, safeStringifyJSON } from 'utils/common/index';
import { addTab } from 'providers/ReduxStore/slices/tabs';
import { updateSettingsSelectedTab } from './index';
import { saveGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
export const renameCollection = (newName, collectionUid) => (dispatch, getState) => {
const state = getState();
@@ -1603,12 +1604,184 @@ export const saveEnvironment = (variables, environmentUid, collectionUid) => (di
});
};
export const mergeAndPersistEnvironment =
({ persistentEnvVariables, collectionUid }) =>
(_dispatch, getState) => {
return new Promise((resolve, reject) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
/**
* Update a variable value directly in the file without affecting draft state
* @param {string} pathname - File path
* @param {Object} variable - Variable object with uid, name, value, type, enabled
* @param {string} scopeType - Type of scope ('request', 'folder', 'collection')
* @param {string} collectionUid - Collection UID
* @param {string} itemUid - Item/Folder UID (for request/folder)
*/
const updateVariableInFile = (pathname, variable, scopeType, collectionUid, itemUid) => (dispatch) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer
.invoke('renderer:update-variable-in-file', pathname, variable, scopeType)
.then(() => {
// Update Redux state to reflect the change
if (scopeType === 'request') {
dispatch({
type: 'collections/updateRequestVarValue',
payload: { collectionUid, itemUid, variable }
});
} else if (scopeType === 'folder') {
dispatch({
type: 'collections/updateFolderVarValue',
payload: { collectionUid, folderUid: itemUid, variable }
});
} else if (scopeType === 'collection') {
dispatch({
type: 'collections/updateCollectionVarValue',
payload: { collectionUid, variable }
});
}
resolve();
})
.catch(reject);
});
};
/**
* Helper: Execute update action with toast notification
* @param {Function} action - The action to dispatch
* @param {string} successMessage - Success toast message
* @returns {Promise}
*/
const executeVariableUpdate = (dispatch, action, successMessage) => {
return dispatch(action)
.then(() => {
toast.success(successMessage);
});
};
/**
* Update a variable value in its detected scope (inline editing)
* @param {string} variableName - Name of the variable to update
* @param {string} newValue - New value for the variable
* @param {Object} scopeInfo - Scope information from getVariableScope()
* @param {string} collectionUid - Collection UID
*/
export const updateVariableInScope = (variableName, newValue, scopeInfo, collectionUid) => (dispatch, getState) => {
return new Promise((resolve, reject) => {
if (!scopeInfo || !variableName) {
return reject(new Error('Invalid scope information or variable name'));
}
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
try {
const { type, data } = scopeInfo;
// Handle read-only variables early
if (type === 'process.env') {
toast.error('Process environment variables cannot be edited');
return reject(new Error('Process environment variables are read-only'));
}
if (type === 'runtime') {
toast.error('Runtime variables are set by scripts and cannot be edited');
return reject(new Error('Runtime variables are read-only'));
}
// Validate collection for non-global scopes
if (type !== 'global' && !collection) {
return reject(new Error('Collection not found'));
}
let updatePromise;
let successMessage;
switch (type) {
case 'environment': {
const { environment, variable } = data;
const updatedVariables = variable
? environment.variables.map((v) => (v.name === variableName ? { ...v, value: newValue } : v))
: [...environment.variables, { uid: uuid(), name: variableName, value: newValue, type: 'text', enabled: true }];
updatePromise = saveEnvironment(updatedVariables, environment.uid, collectionUid);
successMessage = `Variable "${variableName}" ${variable ? 'updated' : 'created'}`;
break;
}
case 'collection': {
const { collection: scopeCollection, variable } = data;
const variableToSave = variable
? { ...variable, value: newValue }
: { uid: uuid(), name: variableName, value: newValue, type: 'text', enabled: true };
const collectionFilePath = path.join(scopeCollection.pathname, 'collection.bru');
updatePromise = updateVariableInFile(collectionFilePath, variableToSave, 'collection', collectionUid, null);
successMessage = `Variable "${variableName}" ${variable ? 'updated' : 'created'}`;
break;
}
case 'folder': {
const { folder, variable } = data;
const variableToSave = variable
? { ...variable, value: newValue }
: { uid: uuid(), name: variableName, value: newValue, type: 'text', enabled: true };
const folderFilePath = path.join(folder.pathname, 'folder.bru');
updatePromise = updateVariableInFile(folderFilePath, variableToSave, 'folder', collectionUid, folder.uid);
successMessage = `Variable "${variableName}" ${variable ? 'updated' : 'created'}`;
break;
}
case 'request': {
const { item, variable } = data;
const variableToSave = variable
? { ...variable, value: newValue }
: { uid: uuid(), name: variableName, value: newValue, type: 'text', enabled: true };
updatePromise = updateVariableInFile(item.pathname, variableToSave, 'request', collectionUid, item.uid);
successMessage = `Variable "${variableName}" ${variable ? 'updated' : 'created'}`;
break;
}
case 'global': {
const globalEnvironments = state.globalEnvironments?.globalEnvironments || [];
const activeGlobalEnvUid = state.globalEnvironments?.activeGlobalEnvironmentUid;
if (!activeGlobalEnvUid) {
return reject(new Error('No active global environment'));
}
const environment = globalEnvironments.find((env) => env.uid === activeGlobalEnvUid);
if (!environment) {
return reject(new Error('Global environment not found'));
}
const updatedVariables = environment.variables.map((v) =>
v.name === variableName ? { ...v, value: newValue } : v);
updatePromise = saveGlobalEnvironment({ variables: updatedVariables, environmentUid: activeGlobalEnvUid });
successMessage = `Variable "${variableName}" updated`;
break;
}
default:
return reject(new Error(`Unknown scope type: ${type}`));
}
executeVariableUpdate(dispatch, updatePromise, successMessage)
.then(resolve)
.catch(reject);
} catch (error) {
toast.error(`Failed to update variable: ${error.message}`);
reject(error);
}
});
};
export const mergeAndPersistEnvironment
= ({ persistentEnvVariables, 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'));

View File

@@ -25,6 +25,19 @@ import path from 'utils/common/path';
import { getUniqueTagsFromItems } from 'utils/collections/index';
import * as exampleReducers from './exampleReducers';
// Helper: Update or create variable in variables array
const updateOrCreateVariable = (vars, variable) => {
const existingVar = vars.find((v) => v.name === variable.name);
if (existingVar) {
// Update existing variable - use the passed variable object to preserve UID
return vars.map((v) => (v.name === variable.name ? variable : v));
}
// Create new variable
return [...vars, variable];
};
// gRPC status code meanings
const grpcStatusCodes = {
0: 'OK',
@@ -3200,8 +3213,42 @@ export const collectionsSlice = createSlice({
deleteResponseExampleFormUrlEncodedParam: exampleReducers.deleteResponseExampleFormUrlEncodedParam,
addResponseExampleMultipartFormParam: exampleReducers.addResponseExampleMultipartFormParam,
updateResponseExampleMultipartFormParam: exampleReducers.updateResponseExampleMultipartFormParam,
deleteResponseExampleMultipartFormParam: exampleReducers.deleteResponseExampleMultipartFormParam
deleteResponseExampleMultipartFormParam: exampleReducers.deleteResponseExampleMultipartFormParam,
/* End Response Example Actions */
updateRequestVarValue: (state, action) => {
const { collectionUid, itemUid, variable } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (!collection) return;
const item = findItemInCollection(collection, itemUid);
if (item) {
const vars = get(item, 'request.vars.req', []);
const updatedVars = updateOrCreateVariable(vars, variable);
set(item, 'request.vars.req', updatedVars);
}
},
updateFolderVarValue: (state, action) => {
const { collectionUid, folderUid, variable } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (!collection) return;
const folder = findItemInCollection(collection, folderUid);
if (folder) {
const vars = get(folder, 'root.request.vars.req', []);
const updatedVars = updateOrCreateVariable(vars, variable);
set(folder, 'root.request.vars.req', updatedVars);
}
},
updateCollectionVarValue: (state, action) => {
const { collectionUid, variable } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (!collection) return;
const vars = get(collection, 'root.request.vars.req', []);
const updatedVars = updateOrCreateVariable(vars, variable);
set(collection, 'root.request.vars.req', updatedVars);
}
}
});
@@ -3375,8 +3422,12 @@ export const {
deleteResponseExampleRequestHeader,
moveResponseExampleRequestHeader,
setResponseExampleRequestHeaders,
setResponseExampleParams
setResponseExampleParams,
/* Response Example Actions - End */
updateRequestVarValue,
updateFolderVarValue,
updateCollectionVarValue
} = collectionsSlice.actions;
export default collectionsSlice.reducer;