mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-15 11:51:30 +00:00
fix: preserve draft state when creating variables via varInfoToolbar (#6152)
* fix: preserve draft state when creating variables via varInfoToolbar
This commit is contained in:
@@ -47,7 +47,13 @@ import {
|
||||
saveRequest as _saveRequest,
|
||||
saveEnvironment as _saveEnvironment,
|
||||
saveCollectionDraft,
|
||||
saveFolderDraft
|
||||
saveFolderDraft,
|
||||
addVar,
|
||||
updateVar,
|
||||
addFolderVar,
|
||||
updateFolderVar,
|
||||
addCollectionVar,
|
||||
updateCollectionVar
|
||||
} from './index';
|
||||
|
||||
import { each } from 'lodash';
|
||||
@@ -1712,58 +1718,6 @@ export const saveEnvironment = (variables, environmentUid, collectionUid) => (di
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -1799,54 +1753,101 @@ export const updateVariableInScope = (variableName, newValue, scopeInfo, collect
|
||||
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;
|
||||
if (!variable) {
|
||||
return reject(new Error('Variable not found'));
|
||||
}
|
||||
|
||||
const updatedVariables = environment.variables.map((v) => (v.uid === variable.uid ? { ...v, value: newValue } : v));
|
||||
|
||||
return dispatch(saveEnvironment(updatedVariables, environment.uid, collectionUid))
|
||||
.then(() => {
|
||||
toast.success(`Variable "${variableName}" updated`);
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
}
|
||||
|
||||
case 'collection': {
|
||||
const { collection: scopeCollection, variable } = data;
|
||||
const variableToSave = variable
|
||||
? { ...variable, value: newValue }
|
||||
: { uid: uuid(), name: variableName, value: newValue, type: 'text', enabled: true };
|
||||
const { variable } = data;
|
||||
|
||||
const collectionFilePath = path.join(scopeCollection.pathname, 'collection.bru');
|
||||
updatePromise = updateVariableInFile(collectionFilePath, variableToSave, 'collection', collectionUid, null);
|
||||
successMessage = `Variable "${variableName}" ${variable ? 'updated' : 'created'}`;
|
||||
break;
|
||||
if (variable) {
|
||||
// Update existing variable in draft
|
||||
dispatch(updateCollectionVar({
|
||||
collectionUid,
|
||||
type: 'request',
|
||||
var: { ...variable, value: newValue }
|
||||
}));
|
||||
} else {
|
||||
// Create new variable in draft with actual values
|
||||
dispatch(addCollectionVar({
|
||||
collectionUid,
|
||||
type: 'request',
|
||||
var: { name: variableName, value: newValue, enabled: true }
|
||||
}));
|
||||
}
|
||||
|
||||
// Save collection root to persist the changes
|
||||
return dispatch(saveCollectionRoot(collectionUid))
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
}
|
||||
|
||||
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;
|
||||
if (variable) {
|
||||
// Update existing variable in draft
|
||||
dispatch(updateFolderVar({
|
||||
collectionUid,
|
||||
folderUid: folder.uid,
|
||||
type: 'request',
|
||||
var: { ...variable, value: newValue }
|
||||
}));
|
||||
} else {
|
||||
// Create new variable in draft with actual values
|
||||
dispatch(addFolderVar({
|
||||
collectionUid,
|
||||
folderUid: folder.uid,
|
||||
type: 'request',
|
||||
var: { name: variableName, value: newValue, enabled: true }
|
||||
}));
|
||||
}
|
||||
|
||||
// Save folder root to persist the changes
|
||||
return dispatch(saveFolderRoot(collectionUid, folder.uid))
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
}
|
||||
|
||||
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;
|
||||
if (variable) {
|
||||
// Update existing variable in draft
|
||||
dispatch(updateVar({
|
||||
collectionUid,
|
||||
itemUid: item.uid,
|
||||
type: 'request',
|
||||
var: { ...variable, value: newValue }
|
||||
}));
|
||||
} else {
|
||||
// Create new variable in draft with actual values
|
||||
dispatch(addVar({
|
||||
collectionUid,
|
||||
itemUid: item.uid,
|
||||
type: 'request',
|
||||
var: { name: variableName, value: newValue, local: false, enabled: true }
|
||||
}));
|
||||
}
|
||||
|
||||
// Save request to persist the changes
|
||||
return dispatch(saveRequest(item.uid, collectionUid, true))
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
}
|
||||
|
||||
case 'global': {
|
||||
@@ -1858,25 +1859,31 @@ export const updateVariableInScope = (variableName, newValue, scopeInfo, collect
|
||||
}
|
||||
|
||||
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);
|
||||
const variable = environment.variables.find((v) => v.name === variableName && v.enabled);
|
||||
|
||||
updatePromise = saveGlobalEnvironment({ variables: updatedVariables, environmentUid: activeGlobalEnvUid });
|
||||
successMessage = `Variable "${variableName}" updated`;
|
||||
break;
|
||||
if (!variable) {
|
||||
return reject(new Error('Variable not found'));
|
||||
}
|
||||
|
||||
const updatedVariables = environment.variables.map((v) =>
|
||||
v.uid === variable.uid ? { ...v, value: newValue } : v);
|
||||
|
||||
return dispatch(saveGlobalEnvironment({ variables: updatedVariables, environmentUid: activeGlobalEnvUid }))
|
||||
.then(() => {
|
||||
toast.success(`Variable "${variableName}" updated`);
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -25,19 +25,6 @@ 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',
|
||||
@@ -1741,6 +1728,7 @@ export const collectionsSlice = createSlice({
|
||||
addVar: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const type = action.payload.type;
|
||||
const varData = action.payload.var || {};
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
@@ -1754,10 +1742,10 @@ export const collectionsSlice = createSlice({
|
||||
item.draft.request.vars.req = item.draft.request.vars.req || [];
|
||||
item.draft.request.vars.req.push({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
local: false,
|
||||
enabled: true
|
||||
name: varData?.name || '',
|
||||
value: varData?.value || '',
|
||||
local: varData?.local !== undefined ? varData.local : false,
|
||||
enabled: varData?.enabled !== undefined ? varData.enabled : true
|
||||
});
|
||||
} else if (type === 'response') {
|
||||
item.draft.request.vars = item.draft.request.vars || {};
|
||||
@@ -2078,6 +2066,7 @@ export const collectionsSlice = createSlice({
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
const type = action.payload.type;
|
||||
const varData = action.payload.var || {};
|
||||
if (folder) {
|
||||
if (!folder.draft) {
|
||||
folder.draft = cloneDeep(folder.root);
|
||||
@@ -2086,9 +2075,9 @@ export const collectionsSlice = createSlice({
|
||||
const vars = get(folder, 'draft.request.vars.req', []);
|
||||
vars.push({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
enabled: true
|
||||
name: varData.name || '',
|
||||
value: varData.value || '',
|
||||
enabled: varData.enabled !== undefined ? varData.enabled : true
|
||||
});
|
||||
set(folder, 'draft.request.vars.req', vars);
|
||||
} else if (type === 'response') {
|
||||
@@ -2097,6 +2086,7 @@ export const collectionsSlice = createSlice({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
local: false,
|
||||
enabled: true
|
||||
});
|
||||
set(folder, 'draft.request.vars.res', vars);
|
||||
@@ -2283,6 +2273,7 @@ export const collectionsSlice = createSlice({
|
||||
addCollectionVar: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const type = action.payload.type;
|
||||
const varData = action.payload.var || {};
|
||||
if (collection) {
|
||||
if (!collection.draft) {
|
||||
collection.draft = {
|
||||
@@ -2293,9 +2284,9 @@ export const collectionsSlice = createSlice({
|
||||
const vars = get(collection, 'draft.root.request.vars.req', []);
|
||||
vars.push({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
enabled: true
|
||||
name: varData.name || '',
|
||||
value: varData.value || '',
|
||||
enabled: varData.enabled !== undefined ? varData.enabled : true
|
||||
});
|
||||
set(collection, 'draft.root.request.vars.req', vars);
|
||||
} else if (type === 'response') {
|
||||
@@ -2304,6 +2295,7 @@ export const collectionsSlice = createSlice({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
local: false,
|
||||
enabled: true
|
||||
});
|
||||
set(collection, 'draft.root.request.vars.res', vars);
|
||||
@@ -3213,42 +3205,8 @@ 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3422,12 +3380,8 @@ export const {
|
||||
deleteResponseExampleRequestHeader,
|
||||
moveResponseExampleRequestHeader,
|
||||
setResponseExampleRequestHeaders,
|
||||
setResponseExampleParams,
|
||||
setResponseExampleParams
|
||||
/* Response Example Actions - End */
|
||||
|
||||
updateRequestVarValue,
|
||||
updateFolderVarValue,
|
||||
updateCollectionVarValue
|
||||
} = collectionsSlice.actions;
|
||||
|
||||
export default collectionsSlice.reducer;
|
||||
|
||||
@@ -189,17 +189,36 @@ export const renderVarInfo = (token, options) => {
|
||||
// Detect variable scope
|
||||
scopeInfo = getVariableScope(variableName, collection, item);
|
||||
|
||||
// If variable doesn't exist in any scope, default to creating it at request level
|
||||
// If variable doesn't exist in any scope, determine scope based on context
|
||||
if (!scopeInfo) {
|
||||
if (item) {
|
||||
// Create as request variable if we have an item context
|
||||
// Determine if item is a folder or request
|
||||
const isFolder = item.type === 'folder';
|
||||
|
||||
if (isFolder) {
|
||||
// We're in folder settings - create as folder variable
|
||||
scopeInfo = {
|
||||
type: 'folder',
|
||||
value: '', // Empty value for new variable
|
||||
data: { folder: item, variable: null } // variable is null since it doesn't exist yet
|
||||
};
|
||||
} else {
|
||||
// We're in a request - create as request variable
|
||||
scopeInfo = {
|
||||
type: 'request',
|
||||
value: '', // Empty value for new variable
|
||||
data: { item, variable: null } // variable is null since it doesn't exist yet
|
||||
};
|
||||
}
|
||||
} else if (collection) {
|
||||
// No item context but we have collection - create as collection variable
|
||||
scopeInfo = {
|
||||
type: 'request',
|
||||
value: '', // Empty value for new variable
|
||||
data: { item, variable: null } // variable is null since it doesn't exist yet
|
||||
type: 'collection',
|
||||
value: '',
|
||||
data: { collection, variable: null }
|
||||
};
|
||||
} else {
|
||||
// If no item context, show as undefined
|
||||
// No context at all, show as undefined
|
||||
scopeInfo = {
|
||||
type: 'undefined',
|
||||
value: '',
|
||||
|
||||
@@ -1280,15 +1280,21 @@ const mergeVars = (collection, requestTreePath = []) => {
|
||||
}
|
||||
});
|
||||
for (let i of requestTreePath) {
|
||||
if (!i) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i.type === 'folder') {
|
||||
let vars = get(i, 'root.request.vars.req', []);
|
||||
// Check draft first, then fall back to root
|
||||
const folderRoot = i.draft || i.root;
|
||||
let vars = get(folderRoot, 'request.vars.req', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
folderVariables[_var.name] = _var.value;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let vars = get(i, 'request.vars.req', []);
|
||||
let vars = i.draft ? get(i, 'draft.request.vars.req', []) : get(i, 'request.vars.req', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
requestVariables[_var.name] = _var.value;
|
||||
@@ -1521,8 +1527,9 @@ export const getVariableScope = (variableName, collection, item) => {
|
||||
}
|
||||
|
||||
// 1. Check Request Variables (highest priority)
|
||||
if (item && item.request && item.request.vars && item.request.vars.req) {
|
||||
const requestVar = item.request.vars.req.find((v) => v.name === variableName && v.enabled);
|
||||
if (item) {
|
||||
const requestVars = item.draft ? get(item, 'draft.request.vars.req', []) : get(item, 'request.vars.req', []);
|
||||
const requestVar = requestVars.find((v) => v.name === variableName && v.enabled);
|
||||
if (requestVar) {
|
||||
return {
|
||||
type: 'request',
|
||||
@@ -1536,8 +1543,14 @@ export const getVariableScope = (variableName, collection, item) => {
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
for (let i = requestTreePath.length - 1; i >= 0; i--) {
|
||||
const pathItem = requestTreePath[i];
|
||||
if (!pathItem) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pathItem.type === 'folder') {
|
||||
const folderVars = get(pathItem, 'root.request.vars.req', []);
|
||||
// Check draft first, then fall back to root
|
||||
const folderRoot = pathItem.draft || pathItem.root;
|
||||
const folderVars = get(folderRoot, 'request.vars.req', []);
|
||||
const folderVar = folderVars.find((v) => v.name === variableName && v.enabled);
|
||||
if (folderVar) {
|
||||
return {
|
||||
@@ -1565,7 +1578,9 @@ export const getVariableScope = (variableName, collection, item) => {
|
||||
}
|
||||
|
||||
// 4. Check Collection Variables
|
||||
const collectionVars = get(collection, 'root.request.vars.req', []);
|
||||
// Check draft first, then fall back to root
|
||||
const collectionRoot = (collection.draft && collection.draft.root) || collection.root || {};
|
||||
const collectionVars = get(collectionRoot, 'request.vars.req', []);
|
||||
const collectionVar = collectionVars.find((v) => v.name === variableName && v.enabled);
|
||||
if (collectionVar) {
|
||||
return {
|
||||
|
||||
@@ -286,72 +286,6 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
}
|
||||
});
|
||||
|
||||
// Helper: Parse file content based on scope type
|
||||
const parseFileByType = async (fileContent, scopeType) => {
|
||||
switch (scopeType) {
|
||||
case 'request':
|
||||
return await parseRequestViaWorker(fileContent);
|
||||
case 'folder':
|
||||
return parseFolder(fileContent);
|
||||
case 'collection':
|
||||
return parseCollection(fileContent);
|
||||
default:
|
||||
throw new Error(`Invalid scope type: ${scopeType}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper: Stringify data based on scope type
|
||||
const stringifyByType = async (data, scopeType) => {
|
||||
switch (scopeType) {
|
||||
case 'request':
|
||||
return await stringifyRequestViaWorker(data);
|
||||
case 'folder':
|
||||
return stringifyFolder(data);
|
||||
case 'collection':
|
||||
return stringifyCollection(data);
|
||||
default:
|
||||
throw new Error(`Invalid scope type: ${scopeType}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper: Update or create variable in array
|
||||
const updateOrCreateVariable = (variables, variable) => {
|
||||
const existingVar = variables.find((v) => v.name === variable.name);
|
||||
|
||||
if (existingVar) {
|
||||
// Update existing variable
|
||||
return variables.map((v) => (v.name === variable.name ? variable : v));
|
||||
}
|
||||
|
||||
// Create new variable
|
||||
return [...variables, variable];
|
||||
};
|
||||
|
||||
// update variable in request/folder/collection file
|
||||
ipcMain.handle('renderer:update-variable-in-file', async (event, pathname, variable, scopeType) => {
|
||||
try {
|
||||
if (!fs.existsSync(pathname)) {
|
||||
throw new Error(`path: ${pathname} does not exist`);
|
||||
}
|
||||
|
||||
// Read and parse the file
|
||||
const fileContent = fs.readFileSync(pathname, 'utf8');
|
||||
const parsedData = await parseFileByType(fileContent, scopeType);
|
||||
|
||||
// Update the specific variable or create it if it doesn't exist
|
||||
const varsPath = 'request.vars.req';
|
||||
const variables = _.get(parsedData, varsPath, []);
|
||||
const updatedVariables = updateOrCreateVariable(variables, variable);
|
||||
|
||||
_.set(parsedData, varsPath, updatedVariables);
|
||||
|
||||
// Stringify and write back
|
||||
const content = await stringifyByType(parsedData, scopeType);
|
||||
await writeFile(pathname, content);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
// create environment
|
||||
ipcMain.handle('renderer:create-environment', async (event, collectionPathname, name, variables) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test, expect } from '../../playwright';
|
||||
import { createCollection, closeAllCollections } from '../utils/page';
|
||||
import { createCollection, closeAllCollections, createRequest } from '../utils/page';
|
||||
|
||||
test.describe('Variable Tooltip', () => {
|
||||
test.afterEach(async ({ page }) => {
|
||||
@@ -43,19 +43,15 @@ test.describe('Variable Tooltip', () => {
|
||||
});
|
||||
|
||||
await test.step('Create request and test tooltip', async () => {
|
||||
// Create request
|
||||
const collectionContainer = page.locator('.collection-name').filter({ hasText: collectionName });
|
||||
await collectionContainer.locator('.collection-actions').hover();
|
||||
await collectionContainer.locator('.collection-actions .icon').click();
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();
|
||||
// Create request using utility method
|
||||
await createRequest(page, 'Test Request', collectionName);
|
||||
|
||||
await page.getByPlaceholder('Request Name').fill('Test Request');
|
||||
await page.locator('#new-request-url .CodeMirror').click();
|
||||
await page.locator('textarea').fill('https://api.example.com?key={{apiKey}}');
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
// Open request
|
||||
// Set the URL
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'Test Request' }).click();
|
||||
const urlEditor = page.locator('#request-url .CodeMirror');
|
||||
await urlEditor.click();
|
||||
await page.keyboard.type('https://api.example.com?key={{apiKey}}');
|
||||
await page.keyboard.press('Control+s');
|
||||
});
|
||||
|
||||
await test.step('Test basic tooltip', async () => {
|
||||
@@ -153,17 +149,15 @@ test.describe('Variable Tooltip', () => {
|
||||
});
|
||||
|
||||
await test.step('Create request with variable references', async () => {
|
||||
const collectionContainer = page.locator('.collection-name').filter({ hasText: collectionName });
|
||||
await collectionContainer.locator('.collection-actions').hover();
|
||||
await collectionContainer.locator('.collection-actions .icon').click();
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();
|
||||
|
||||
await page.getByPlaceholder('Request Name').fill('Ref Test Request');
|
||||
await page.locator('#new-request-url .CodeMirror').click();
|
||||
await page.locator('textarea').fill('{{endpoint}}');
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
// Create request using utility method
|
||||
await createRequest(page, 'Ref Test Request', collectionName);
|
||||
|
||||
// Set the URL
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'Ref Test Request' }).click();
|
||||
const urlEditor = page.locator('#request-url .CodeMirror');
|
||||
await urlEditor.click();
|
||||
await page.keyboard.type('{{endpoint}}');
|
||||
await page.keyboard.press('Control+s');
|
||||
});
|
||||
|
||||
await test.step('Test variable referencing other variables', async () => {
|
||||
@@ -272,18 +266,15 @@ test.describe('Variable Tooltip', () => {
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByText('×').click();
|
||||
|
||||
// Create request
|
||||
const collectionContainer = page.locator('.collection-name').filter({ hasText: collectionName });
|
||||
await collectionContainer.locator('.collection-actions').hover();
|
||||
await collectionContainer.locator('.collection-actions .icon').click();
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();
|
||||
|
||||
await page.getByPlaceholder('Request Name').fill('Readonly Test');
|
||||
await page.locator('#new-request-url .CodeMirror').click();
|
||||
await page.locator('textarea').fill('https://example.com');
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
// Create request using utility method
|
||||
await createRequest(page, 'Readonly Test', collectionName);
|
||||
|
||||
// Set the URL
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'Readonly Test' }).click();
|
||||
const urlEditor = page.locator('#request-url .CodeMirror');
|
||||
await urlEditor.click();
|
||||
await page.keyboard.type('https://example.com');
|
||||
await page.keyboard.press('Control+s');
|
||||
});
|
||||
|
||||
await test.step('Test process.env variable tooltip', async () => {
|
||||
@@ -314,4 +305,114 @@ test.describe('Variable Tooltip', () => {
|
||||
await expect(tooltip.locator('.var-value-editor')).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test('should auto-save request when creating variable via tooltip', async ({ page, createTmpDir }) => {
|
||||
const collectionName = 'draft-autosave-test';
|
||||
|
||||
await test.step('Setup collection and request', async () => {
|
||||
await createCollection(page, collectionName, await createTmpDir('draft-autosave'), {
|
||||
openWithSandboxMode: 'safe'
|
||||
});
|
||||
|
||||
// Create request using utility method
|
||||
await createRequest(page, 'Autosave Test', collectionName);
|
||||
|
||||
// Set the URL
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'Autosave Test' }).click();
|
||||
const urlEditor = page.locator('#request-url .CodeMirror');
|
||||
await urlEditor.click();
|
||||
await page.keyboard.type('https://api.example.com');
|
||||
await page.keyboard.press('Control+s');
|
||||
});
|
||||
|
||||
await test.step('Edit URL to create draft with undefined variable', async () => {
|
||||
// Edit the URL to add a variable reference
|
||||
const urlEditor = page.locator('#request-url .CodeMirror');
|
||||
await urlEditor.click();
|
||||
await page.keyboard.press('End');
|
||||
await page.keyboard.type('/users/{{myApiKey}}');
|
||||
|
||||
// Verify draft indicator appears (unsaved changes) in the request tab
|
||||
const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Autosave Test' }) });
|
||||
await expect(requestTab.locator('.has-changes-icon')).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Create variable via tooltip - should auto-save entire request', async () => {
|
||||
// Hover over the undefined variable {{myApiKey}}
|
||||
const urlEditor = page.locator('#request-url .CodeMirror');
|
||||
const undefinedVar = urlEditor.locator('.cm-variable-invalid').filter({ hasText: 'myApiKey' }).first();
|
||||
await undefinedVar.hover();
|
||||
|
||||
// Tooltip should appear
|
||||
const tooltip = page.locator('.CodeMirror-brunoVarInfo').first();
|
||||
await expect(tooltip).toBeVisible();
|
||||
await expect(tooltip.locator('.var-name')).toContainText('myApiKey');
|
||||
await expect(tooltip.locator('.var-scope-badge')).toContainText('Request');
|
||||
|
||||
// Click to edit the variable
|
||||
const valueDisplay = tooltip.locator('.var-value-editable-display');
|
||||
await valueDisplay.click();
|
||||
|
||||
// Type value
|
||||
const editor = tooltip.locator('.var-value-editor .CodeMirror');
|
||||
await expect(editor).toBeVisible();
|
||||
await page.keyboard.type('secret-key-123');
|
||||
|
||||
// Click outside to close editor - this will auto-save the entire request
|
||||
await page.locator('body').click();
|
||||
});
|
||||
|
||||
await test.step('Verify request was auto-saved with URL changes and new variable', async () => {
|
||||
// Move mouse away
|
||||
await page.mouse.move(0, 0);
|
||||
|
||||
// Verify variable is now valid (green) in the URL
|
||||
const urlEditor = page.locator('#request-url .CodeMirror');
|
||||
const validVar = urlEditor.locator('.cm-variable-valid').filter({ hasText: 'myApiKey' });
|
||||
await expect(validVar).toBeVisible();
|
||||
|
||||
// Hover to verify value was saved
|
||||
await validVar.first().hover();
|
||||
const tooltip = page.locator('.CodeMirror-brunoVarInfo').first();
|
||||
await expect(tooltip).toBeVisible();
|
||||
await expect(tooltip.locator('.var-value-editable-display')).toContainText('secret-key-123');
|
||||
|
||||
// Move mouse away
|
||||
await page.mouse.move(0, 0);
|
||||
|
||||
// Verify the URL changes were also saved
|
||||
const urlContent = await urlEditor.locator('.CodeMirror-line').first().textContent();
|
||||
expect(urlContent).toContain('api.example.com/users');
|
||||
expect(urlContent).toContain('myApiKey');
|
||||
|
||||
// Verify draft indicator is GONE (everything was auto-saved)
|
||||
const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Autosave Test' }) });
|
||||
await expect(requestTab.locator('.has-changes-icon')).not.toBeVisible();
|
||||
await expect(requestTab.locator('.close-icon')).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Verify variable exists in Vars tab', async () => {
|
||||
// Check variable is saved to file - should appear in the Vars tab
|
||||
await page.getByRole('tab', { name: 'Vars' }).click();
|
||||
|
||||
// The variable should exist in the saved file
|
||||
const varsTable = page.locator('table').first();
|
||||
await expect(varsTable).toBeVisible();
|
||||
|
||||
const varRow = varsTable.locator('tbody tr').first();
|
||||
await expect(varRow).toBeVisible();
|
||||
|
||||
// Check variable name
|
||||
const varNameInput = varRow.locator('td').first().locator('input[type="text"]');
|
||||
await expect(varNameInput).toBeVisible();
|
||||
await expect(varNameInput).toHaveValue('myApiKey');
|
||||
|
||||
// Check variable value
|
||||
const varValueTd = varRow.locator('td').nth(1);
|
||||
const varValue = varValueTd.locator('.CodeMirror');
|
||||
await expect(varValue).toBeVisible();
|
||||
const varValueContent = await varValue.locator('.CodeMirror-line').textContent();
|
||||
expect(varValueContent).toContain('secret-key-123');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user