fix: validate environment variables in unsaved changes dialog (#7403)

This commit is contained in:
Chirag Chandrashekhar
2026-04-03 17:05:05 +05:30
committed by GitHub
parent 5db34dff11
commit 073b1ef036
4 changed files with 136 additions and 6 deletions

View File

@@ -22,6 +22,7 @@ import NewRequest from 'components/Sidebar/NewRequest/index';
import GradientCloseButton from './GradientCloseButton';
import { flattenItems } from 'utils/collections/index';
import { closeWsConnection } from 'utils/network/index';
import { getInvalidVariableNames } from 'utils/common/variables';
import ExampleTab from '../ExampleTab';
import toast from 'react-hot-toast';
@@ -356,6 +357,11 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
window.addEventListener('dotenv-save-failed', onFailed, { once: true });
window.dispatchEvent(new Event('dotenv-save'));
} else if (draft?.environmentUid && draft?.variables) {
const invalidNames = getInvalidVariableNames(draft.variables);
if (invalidNames.length > 0) {
toast.error(`Invalid variable name(s): ${invalidNames.join(', ')}`);
return;
}
dispatch(saveEnvironment(draft.variables, draft.environmentUid, collection.uid))
.then(() => {
dispatch(clearEnvironmentsDraft({ collectionUid: collection.uid }));
@@ -402,6 +408,11 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
window.addEventListener('dotenv-save-failed', onFailed, { once: true });
window.dispatchEvent(new Event('dotenv-save'));
} else if (draft?.environmentUid && draft?.variables) {
const invalidNames = getInvalidVariableNames(draft.variables);
if (invalidNames.length > 0) {
toast.error(`Invalid variable name(s): ${invalidNames.join(', ')}`);
return;
}
dispatch(saveGlobalEnvironment({ variables: draft.variables, environmentUid: draft.environmentUid }))
.then(() => {
dispatch(clearGlobalEnvironmentDraft());

View File

@@ -6,6 +6,7 @@ import { useSelector } from 'react-redux';
import { useDispatch } from 'react-redux';
import { findCollectionByUid, flattenItems, isItemARequest, hasRequestChanges, findEnvironmentInCollection } from 'utils/collections';
import { pluralizeWord } from 'utils/common';
import { getInvalidVariableNames } from 'utils/common/variables';
import { completeQuitFlow } from 'providers/ReduxStore/slices/app';
import { saveMultipleRequests, saveMultipleCollections, saveMultipleFolders, saveEnvironment, closeTabs } from 'providers/ReduxStore/slices/collections/actions';
import { saveGlobalEnvironment, clearGlobalEnvironmentDraft } from 'providers/ReduxStore/slices/global-environments';
@@ -13,6 +14,7 @@ import { deleteRequestDraft, deleteCollectionDraft, deleteFolderDraft, clearEnvi
import { IconAlertTriangle } from '@tabler/icons';
import Modal from 'components/Modal';
import Button from 'ui/Button';
import toast from 'react-hot-toast';
const SaveRequestsModal = ({ onClose, forceCloseTabs = false, tabUidsToClose = [] }) => {
const MAX_UNSAVED_ITEMS_TO_SHOW = 5;
@@ -166,14 +168,27 @@ const SaveRequestsModal = ({ onClose, forceCloseTabs = false, tabUidsToClose = [
await dispatch(saveMultipleRequests(requestDrafts));
}
// Save all collection environment drafts
for (const draft of collectionEnvironmentDrafts) {
await dispatch(saveEnvironment(draft.variables, draft.environmentUid, draft.collectionUid));
// Save environment drafts, skipping any with invalid variable names
const allEnvironmentDrafts = [...collectionEnvironmentDrafts, ...globalEnvironmentDrafts];
let hasSkippedEnvs = false;
for (const draft of allEnvironmentDrafts) {
const invalidNames = getInvalidVariableNames(draft.variables);
if (invalidNames.length > 0) {
hasSkippedEnvs = true;
toast.error(`Cannot save "${draft.name}": invalid variable name(s) — ${invalidNames.join(', ')}`);
continue;
}
if (draft.type === 'collection-environment') {
await dispatch(saveEnvironment(draft.variables, draft.environmentUid, draft.collectionUid));
} else {
await dispatch(saveGlobalEnvironment({ variables: draft.variables, environmentUid: draft.environmentUid }));
}
}
// Save all global environment drafts
for (const draft of globalEnvironmentDrafts) {
await dispatch(saveGlobalEnvironment({ variables: draft.variables, environmentUid: draft.environmentUid }));
if (hasSkippedEnvs) {
return;
}
if (forceCloseTabs) {

View File

@@ -1,6 +1,7 @@
const { describe, it, expect } = require('@jest/globals');
import { sanitizeName, validateName } from './regex';
import { hasInvalidVariableNames } from './variables';
describe('regex validators', () => {
describe('sanitize name', () => {
@@ -163,3 +164,85 @@ describe('sanitizeName and validateName', () => {
});
});
});
describe('hasInvalidVariableNames', () => {
describe('valid variable names', () => {
it('should return false for alphanumeric names with underscores, hyphens, and dots', () => {
const validVariables = [
{ name: 'valid_name', value: 'test' },
{ name: 'valid-name', value: 'test' },
{ name: 'valid.name', value: 'test' },
{ name: 'validName123', value: 'test' }
];
expect(hasInvalidVariableNames(validVariables)).toBe(false);
});
it('should return false for empty array (no variables to validate)', () => {
expect(hasInvalidVariableNames([])).toBe(false);
});
});
describe('invalid variable names', () => {
it('should return true for variable names with unicode letters (not in \\w)', () => {
const variables = [{ name: 'válid_ñame', value: 'test' }];
expect(hasInvalidVariableNames(variables)).toBe(true);
});
it('should return true for variable names containing spaces', () => {
const variables = [{ name: 'invalid name', value: 'test' }];
expect(hasInvalidVariableNames(variables)).toBe(true);
});
it('should return true for variable names with special characters', () => {
const invalidVariables = [
{ name: 'invalid@name', value: 'test' },
{ name: 'invalid!name', value: 'test' },
{ name: 'invalid#name', value: 'test' },
{ name: 'invalid$name', value: 'test' }
];
invalidVariables.forEach((variable) => {
expect(hasInvalidVariableNames([variable])).toBe(true);
});
});
it('should return true if any variable in array has invalid name', () => {
const mixedVariables = [
{ name: 'valid_name', value: 'test' },
{ name: 'invalid name', value: 'test' },
{ name: 'another_valid', value: 'test' }
];
expect(hasInvalidVariableNames(mixedVariables)).toBe(true);
});
});
describe('empty/placeholder variable names (skipped)', () => {
it('should skip variables with empty names (placeholder rows in UI)', () => {
const variables = [
{ name: '', value: 'test' },
{ name: ' ', value: 'test' }
];
expect(hasInvalidVariableNames(variables)).toBe(false);
});
it('should skip variables without name property', () => {
const variables = [
{ value: 'test' },
{ name: null, value: 'test' }
];
expect(hasInvalidVariableNames(variables)).toBe(false);
});
});
describe('defensive handling of invalid inputs', () => {
it('should return false for null or undefined (no variables to validate)', () => {
expect(hasInvalidVariableNames(null)).toBe(false);
expect(hasInvalidVariableNames(undefined)).toBe(false);
});
it('should return false for non-array inputs (defensive check)', () => {
expect(hasInvalidVariableNames('string')).toBe(false);
expect(hasInvalidVariableNames(123)).toBe(false);
expect(hasInvalidVariableNames({})).toBe(false);
});
});
});

View File

@@ -0,0 +1,21 @@
import { variableNameRegex } from './regex';
/**
* Returns the list of invalid variable names from a variables array.
* Skips empty/placeholder names (empty string or whitespace-only).
*/
export const getInvalidVariableNames = (variables) => {
if (!variables || !Array.isArray(variables)) return [];
return variables
.filter((variable) => variable.name && variable.name.trim() !== '' && !variableNameRegex.test(variable.name))
.map((variable) => variable.name);
};
/**
* Checks whether any variable in the array has an invalid name.
* Uses variableNameRegex from regex.js — names may only contain
* word characters (\\w), hyphens, and dots.
*/
export const hasInvalidVariableNames = (variables) => {
return getInvalidVariableNames(variables).length > 0;
};