From 249f31878775f1dff9d9c7dfe029c20e9a4d5906 Mon Sep 17 00:00:00 2001
From: Its-treason <39559178+Its-treason@users.noreply.github.com>
Date: Wed, 4 Oct 2023 16:55:15 +0200
Subject: [PATCH 001/209] fix: Fix name validation consistency
---
.../EnvironmentSettings/CreateEnvironment/index.js | 2 ++
.../EnvironmentSettings/RenameEnvironment/index.js | 2 ++
.../Collection/CollectionItem/RenameCollectionItem/index.js | 2 ++
.../src/components/Sidebar/CreateCollection/index.js | 3 ++-
packages/bruno-app/src/components/Sidebar/NewFolder/index.js | 2 ++
.../bruno-app/src/components/Sidebar/NewRequest/index.js | 2 ++
packages/bruno-app/src/utils/common/regex.js | 3 +++
packages/bruno-electron/src/ipc/collection.js | 5 -----
8 files changed, 15 insertions(+), 6 deletions(-)
create mode 100644 packages/bruno-app/src/utils/common/regex.js
diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/CreateEnvironment/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/CreateEnvironment/index.js
index d412687e25..567ce9957e 100644
--- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/CreateEnvironment/index.js
+++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/CreateEnvironment/index.js
@@ -6,6 +6,7 @@ import { useFormik } from 'formik';
import { addEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import * as Yup from 'yup';
import { useDispatch } from 'react-redux';
+import { filenameRegex } from 'utils/common/regex';
const CreateEnvironment = ({ collection, onClose }) => {
const dispatch = useDispatch();
@@ -19,6 +20,7 @@ const CreateEnvironment = ({ collection, onClose }) => {
name: Yup.string()
.min(1, 'must be atleast 1 characters')
.max(50, 'must be 50 characters or less')
+ .matches(filenameRegex, 'Folder name contains invalid characters')
.required('name is required')
}),
onSubmit: (values) => {
diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/RenameEnvironment/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/RenameEnvironment/index.js
index dc928d4c68..dd10b33651 100644
--- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/RenameEnvironment/index.js
+++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/RenameEnvironment/index.js
@@ -6,6 +6,7 @@ import { useFormik } from 'formik';
import { renameEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import * as Yup from 'yup';
import { useDispatch } from 'react-redux';
+import { filenameRegex } from 'utils/common/regex';
const RenameEnvironment = ({ onClose, environment, collection }) => {
const dispatch = useDispatch();
@@ -19,6 +20,7 @@ const RenameEnvironment = ({ onClose, environment, collection }) => {
name: Yup.string()
.min(1, 'must be atleast 1 characters')
.max(50, 'must be 50 characters or less')
+ .matches(filenameRegex, 'Folder name contains invalid characters')
.required('name is required')
}),
onSubmit: (values) => {
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js
index 9b485e9928..8acfeb3b4f 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js
@@ -5,6 +5,7 @@ import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
import { isItemAFolder } from 'utils/tabs';
import { renameItem } from 'providers/ReduxStore/slices/collections/actions';
+import { filenameRegex } from 'utils/common/regex';
const RenameCollectionItem = ({ collection, item, onClose }) => {
const dispatch = useDispatch();
@@ -19,6 +20,7 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
name: Yup.string()
.min(1, 'must be atleast 1 characters')
.max(50, 'must be 50 characters or less')
+ .matches(filenameRegex, `${isFolder ? 'folder' : 'request'} name contains invalid characters`)
.required('name is required')
}),
onSubmit: (values) => {
diff --git a/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js b/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js
index 9b56ca1b87..0dcba27bab 100644
--- a/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js
+++ b/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js
@@ -7,6 +7,7 @@ import { createCollection } from 'providers/ReduxStore/slices/collections/action
import toast from 'react-hot-toast';
import Tooltip from 'components/Tooltip';
import Modal from 'components/Modal';
+import { filenameRegex } from 'utils/common/regex';
const CreateCollection = ({ onClose }) => {
const inputRef = useRef();
@@ -27,7 +28,7 @@ const CreateCollection = ({ onClose }) => {
collectionFolderName: Yup.string()
.min(1, 'must be atleast 1 characters')
.max(50, 'must be 50 characters or less')
- .matches(/^[\w\-. ]+$/, 'Folder name contains invalid characters')
+ .matches(filenameRegex, 'Folder name contains invalid characters')
.required('folder name is required'),
collectionLocation: Yup.string()
.min(1, 'location is required')
diff --git a/packages/bruno-app/src/components/Sidebar/NewFolder/index.js b/packages/bruno-app/src/components/Sidebar/NewFolder/index.js
index 9245d7abc6..1513b811b6 100644
--- a/packages/bruno-app/src/components/Sidebar/NewFolder/index.js
+++ b/packages/bruno-app/src/components/Sidebar/NewFolder/index.js
@@ -5,6 +5,7 @@ import * as Yup from 'yup';
import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
import { newFolder } from 'providers/ReduxStore/slices/collections/actions';
+import { filenameRegex } from 'utils/common/regex';
const NewFolder = ({ collection, item, onClose }) => {
const dispatch = useDispatch();
@@ -18,6 +19,7 @@ const NewFolder = ({ collection, item, onClose }) => {
folderName: Yup.string()
.min(1, 'must be atleast 1 characters')
.required('name is required')
+ .matches(filenameRegex, 'Folder name contains invalid characters')
.test({
name: 'folderName',
message: 'The folder name "environments" at the root of the collection is reserved in bruno',
diff --git a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js
index f5753aced0..764ee1e29d 100644
--- a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js
+++ b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js
@@ -11,6 +11,7 @@ import { addTab } from 'providers/ReduxStore/slices/tabs';
import HttpMethodSelector from 'components/RequestPane/QueryUrl/HttpMethodSelector';
import { getDefaultRequestPaneTab } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
+import { filenameRegex } from 'utils/common/regex';
const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
const dispatch = useDispatch();
@@ -27,6 +28,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
requestName: Yup.string()
.min(1, 'must be atleast 1 characters')
.required('name is required')
+ .matches(filenameRegex, 'request name contains invalid characters')
.test({
name: 'requestName',
message: 'The request name "index" is reserved in bruno',
diff --git a/packages/bruno-app/src/utils/common/regex.js b/packages/bruno-app/src/utils/common/regex.js
new file mode 100644
index 0000000000..f9c109580d
--- /dev/null
+++ b/packages/bruno-app/src/utils/common/regex.js
@@ -0,0 +1,3 @@
+// Regex for validating filenames that covers most cases
+// See https://github.com/usebruno/bruno/pull/349 for more info
+export const filenameRegex = /^(?!CON|PRN|AUX|NUL|COM\d|LPT\d|^ |^\-)[\w\-\. \(\)\[\]]+[^\. ]$/
diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js
index ae85558afe..5e7b4b55d2 100644
--- a/packages/bruno-electron/src/ipc/collection.js
+++ b/packages/bruno-electron/src/ipc/collection.js
@@ -5,7 +5,6 @@ const { ipcMain, shell } = require('electron');
const { envJsonToBru, bruToJson, jsonToBru } = require('../bru');
const {
- isValidPathname,
writeFile,
hasBruExtension,
isDirectory,
@@ -51,10 +50,6 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
throw new Error(`collection: ${dirPath} already exists`);
}
- if (!isValidPathname(dirPath)) {
- throw new Error(`collection: invalid pathname - ${dir}`);
- }
-
await createDirectory(dirPath);
const uid = generateUidBasedOnHash(dirPath);
From ccfff8870ef4fd7614fc2809d1c909239c84a579 Mon Sep 17 00:00:00 2001
From: Its-treason <39559178+Its-treason@users.noreply.github.com>
Date: Fri, 6 Oct 2023 21:59:49 +0200
Subject: [PATCH 002/209] feat: Allow special characters in request and
environment names
---
.../CreateEnvironment/index.js | 4 +-
.../RenameEnvironment/index.js | 4 +-
.../RenameCollectionItem/index.js | 7 +-
.../Sidebar/CreateCollection/index.js | 7 +-
.../src/components/Sidebar/NewFolder/index.js | 8 +-
.../components/Sidebar/NewRequest/index.js | 1 -
.../ReduxStore/slices/collections/actions.js | 26 ++---
.../ReduxStore/slices/collections/index.js | 5 +
packages/bruno-app/src/utils/common/regex.js | 4 +-
packages/bruno-electron/src/app/watcher.js | 12 ++-
packages/bruno-electron/src/ipc/collection.js | 100 ++++++++++++------
.../bruno-electron/src/utils/filesystem.js | 7 +-
packages/bruno-lang/v2/src/envToJson.js | 19 +++-
packages/bruno-lang/v2/src/jsonToEnv.js | 8 +-
14 files changed, 136 insertions(+), 76 deletions(-)
diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/CreateEnvironment/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/CreateEnvironment/index.js
index 567ce9957e..15c4efa008 100644
--- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/CreateEnvironment/index.js
+++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/CreateEnvironment/index.js
@@ -6,7 +6,6 @@ import { useFormik } from 'formik';
import { addEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import * as Yup from 'yup';
import { useDispatch } from 'react-redux';
-import { filenameRegex } from 'utils/common/regex';
const CreateEnvironment = ({ collection, onClose }) => {
const dispatch = useDispatch();
@@ -19,8 +18,7 @@ const CreateEnvironment = ({ collection, onClose }) => {
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be atleast 1 characters')
- .max(50, 'must be 50 characters or less')
- .matches(filenameRegex, 'Folder name contains invalid characters')
+ .max(250, 'must be 250 characters or less')
.required('name is required')
}),
onSubmit: (values) => {
diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/RenameEnvironment/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/RenameEnvironment/index.js
index dd10b33651..121c90b073 100644
--- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/RenameEnvironment/index.js
+++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/RenameEnvironment/index.js
@@ -6,7 +6,6 @@ import { useFormik } from 'formik';
import { renameEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import * as Yup from 'yup';
import { useDispatch } from 'react-redux';
-import { filenameRegex } from 'utils/common/regex';
const RenameEnvironment = ({ onClose, environment, collection }) => {
const dispatch = useDispatch();
@@ -19,8 +18,7 @@ const RenameEnvironment = ({ onClose, environment, collection }) => {
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be atleast 1 characters')
- .max(50, 'must be 50 characters or less')
- .matches(filenameRegex, 'Folder name contains invalid characters')
+ .max(250, 'must be 250 characters or less')
.required('name is required')
}),
onSubmit: (values) => {
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js
index 8acfeb3b4f..5211efdc80 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js
@@ -5,7 +5,7 @@ import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
import { isItemAFolder } from 'utils/tabs';
import { renameItem } from 'providers/ReduxStore/slices/collections/actions';
-import { filenameRegex } from 'utils/common/regex';
+import { dirnameRegex } from 'utils/common/regex';
const RenameCollectionItem = ({ collection, item, onClose }) => {
const dispatch = useDispatch();
@@ -19,8 +19,9 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be atleast 1 characters')
- .max(50, 'must be 50 characters or less')
- .matches(filenameRegex, `${isFolder ? 'folder' : 'request'} name contains invalid characters`)
+ .max(250, 'must be 250 characters or less')
+ .trim()
+ .matches(isFolder ? dirnameRegex : /.*/g, `${isFolder ? 'folder' : 'request'} name contains invalid characters`)
.required('name is required')
}),
onSubmit: (values) => {
diff --git a/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js b/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js
index 49608c7b09..3f18c8e56b 100644
--- a/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js
+++ b/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js
@@ -7,7 +7,7 @@ import { createCollection } from 'providers/ReduxStore/slices/collections/action
import toast from 'react-hot-toast';
import Tooltip from 'components/Tooltip';
import Modal from 'components/Modal';
-import { filenameRegex } from 'utils/common/regex';
+import { dirnameRegex } from 'utils/common/regex';
const CreateCollection = ({ onClose }) => {
const inputRef = useRef();
@@ -27,8 +27,9 @@ const CreateCollection = ({ onClose }) => {
.required('collection name is required'),
collectionFolderName: Yup.string()
.min(1, 'must be atleast 1 characters')
- .max(50, 'must be 50 characters or less')
- .matches(filenameRegex, 'Folder name contains invalid characters')
+ .max(250, 'must be 250 characters or less')
+ .trim()
+ .matches(dirnameRegex, 'Folder name contains invalid characters')
.required('folder name is required'),
collectionLocation: Yup.string().min(1, 'location is required').required('location is required')
}),
diff --git a/packages/bruno-app/src/components/Sidebar/NewFolder/index.js b/packages/bruno-app/src/components/Sidebar/NewFolder/index.js
index 1513b811b6..8b0826cdd4 100644
--- a/packages/bruno-app/src/components/Sidebar/NewFolder/index.js
+++ b/packages/bruno-app/src/components/Sidebar/NewFolder/index.js
@@ -5,7 +5,7 @@ import * as Yup from 'yup';
import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
import { newFolder } from 'providers/ReduxStore/slices/collections/actions';
-import { filenameRegex } from 'utils/common/regex';
+import { dirnameRegex } from 'utils/common/regex';
const NewFolder = ({ collection, item, onClose }) => {
const dispatch = useDispatch();
@@ -17,9 +17,11 @@ const NewFolder = ({ collection, item, onClose }) => {
},
validationSchema: Yup.object({
folderName: Yup.string()
- .min(1, 'must be atleast 1 characters')
.required('name is required')
- .matches(filenameRegex, 'Folder name contains invalid characters')
+ .min(1, 'must be atleast 1 characters')
+ .max(250, 'must be 250 characters or less')
+ .trim()
+ .matches(dirnameRegex, 'Folder name contains invalid characters')
.test({
name: 'folderName',
message: 'The folder name "environments" at the root of the collection is reserved in bruno',
diff --git a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js
index 764ee1e29d..c8ff66dff7 100644
--- a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js
+++ b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js
@@ -28,7 +28,6 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
requestName: Yup.string()
.min(1, 'must be atleast 1 characters')
.required('name is required')
- .matches(filenameRegex, 'request name contains invalid characters')
.test({
name: 'requestName',
message: 'The request name "index" is reserved in bruno',
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 7d5577c869..4ce535d290 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
@@ -250,7 +250,7 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta
if (!collection) {
return reject(new Error('Collection not found'));
}
-
+ console.log(collection);
const collectionCopy = cloneDeep(collection);
const item = findItemInCollection(collectionCopy, itemUid);
if (!item) {
@@ -258,18 +258,10 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta
}
const dirname = getDirectoryName(item.pathname);
-
- let newPathname = '';
- if (item.type === 'folder') {
- newPathname = path.join(dirname, trim(newName));
- } else {
- const filename = resolveRequestFilename(newName);
- newPathname = path.join(dirname, filename);
- }
const { ipcRenderer } = window;
ipcRenderer
- .invoke('renderer:rename-item', item.pathname, newPathname, newName)
+ .invoke('renderer:rename-item', item.pathname, dirname, newName)
.then(() => {
// In case of Mac and Linux, we get the unlinkDir and addDir IPC events from electron which takes care of updating the state
// But in windows we don't get those events, so we need to update the state manually
@@ -312,14 +304,13 @@ export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getStat
(i) => i.type !== 'folder' && trim(i.filename) === trim(filename)
);
if (!reqWithSameNameExists) {
- const fullName = `${collection.pathname}${PATH_SEPARATOR}${filename}`;
const { ipcRenderer } = window;
const requestItems = filter(collection.items, (i) => i.type !== 'folder');
itemToSave.seq = requestItems ? requestItems.length + 1 : 1;
itemSchema
.validate(itemToSave)
- .then(() => ipcRenderer.invoke('renderer:new-request', fullName, itemToSave))
+ .then(() => ipcRenderer.invoke('renderer:new-request', collection.pathname, itemToSave))
.then(resolve)
.catch(reject);
} else {
@@ -331,15 +322,14 @@ export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getStat
(i) => i.type !== 'folder' && trim(i.filename) === trim(filename)
);
if (!reqWithSameNameExists) {
- const dirname = getDirectoryName(item.pathname);
- const fullName = path.join(dirname, filename);
+ const pathname = getDirectoryName(item.pathname);
const { ipcRenderer } = window;
const requestItems = filter(parentItem.items, (i) => i.type !== 'folder');
itemToSave.seq = requestItems ? requestItems.length + 1 : 1;
itemSchema
.validate(itemToSave)
- .then(() => ipcRenderer.invoke('renderer:new-request', fullName, itemToSave))
+ .then(() => ipcRenderer.invoke('renderer:new-request', pathname, itemToSave))
.then(resolve)
.catch(reject);
} else {
@@ -592,10 +582,9 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
item.seq = requestItems.length + 1;
if (!reqWithSameNameExists) {
- const fullName = `${collection.pathname}${PATH_SEPARATOR}${filename}`;
const { ipcRenderer } = window;
- ipcRenderer.invoke('renderer:new-request', fullName, item).then(resolve).catch(reject);
+ ipcRenderer.invoke('renderer:new-request', collection.pathname, item).then(resolve).catch(reject);
// Add the new request name here so it can be opened in a new tab in useCollectionTreeSync.js
dispatch(updateLastAction({ lastAction: { type: 'ADD_REQUEST', payload: item.name }, collectionUid }));
} else {
@@ -611,10 +600,9 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
const requestItems = filter(currentItem.items, (i) => i.type !== 'folder');
item.seq = requestItems.length + 1;
if (!reqWithSameNameExists) {
- const fullName = `${currentItem.pathname}${PATH_SEPARATOR}${filename}`;
const { ipcRenderer } = window;
- ipcRenderer.invoke('renderer:new-request', fullName, item).then(resolve).catch(reject);
+ ipcRenderer.invoke('renderer:new-request', currentItem.pathname, item).then(resolve).catch(reject);
// Add the new request name here so it can be opened in a new tab in useCollectionTreeSync.js
dispatch(updateLastAction({ lastAction: { type: 'ADD_REQUEST', payload: item.name }, collectionUid }));
} else {
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 ec89bb85da..5c103d05f0 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
@@ -161,6 +161,9 @@ export const collectionsSlice = createSlice({
if (item) {
item.name = action.payload.newName;
+ if (item.type === 'folder') {
+ item.pathname = path.join(path.dirname(item.pathname), action.payload.newName);
+ }
}
}
},
@@ -962,6 +965,7 @@ export const collectionsSlice = createSlice({
}
},
collectionAddDirectoryEvent: (state, action) => {
+ console.log('ADD DIR', action.payload);
const { dir } = action.payload;
const collection = findCollectionByUid(state.collections, dir.meta.collectionUid);
@@ -1049,6 +1053,7 @@ export const collectionsSlice = createSlice({
if (existingEnv) {
existingEnv.variables = environment.variables;
+ existingEnv.name = environment.name;
} else {
collection.environments.push(environment);
diff --git a/packages/bruno-app/src/utils/common/regex.js b/packages/bruno-app/src/utils/common/regex.js
index f9c109580d..6c348a8907 100644
--- a/packages/bruno-app/src/utils/common/regex.js
+++ b/packages/bruno-app/src/utils/common/regex.js
@@ -1,3 +1,3 @@
-// Regex for validating filenames that covers most cases
// See https://github.com/usebruno/bruno/pull/349 for more info
-export const filenameRegex = /^(?!CON|PRN|AUX|NUL|COM\d|LPT\d|^ |^\-)[\w\-\. \(\)\[\]]+[^\. ]$/
+// Scrict regex for validating directories. Covers most edge cases like windows device names
+export const dirnameRegex = /^(?!CON|PRN|AUX|NUL|COM\d|LPT\d|^ |^\-)[\w\-\. \(\)\[\]\!]+[^\. ]$/;
diff --git a/packages/bruno-electron/src/app/watcher.js b/packages/bruno-electron/src/app/watcher.js
index c5973e79cb..5fadb9f265 100644
--- a/packages/bruno-electron/src/app/watcher.js
+++ b/packages/bruno-electron/src/app/watcher.js
@@ -93,7 +93,10 @@ const addEnvironmentFile = async (win, pathname, collectionUid, collectionPath)
}
file.data = bruToEnvJson(bruContent);
- file.data.name = basename.substring(0, basename.length - 4);
+ // Older env files do not have a meta block
+ if (!file.data.name) {
+ file.data.name = basename.substring(0, basename.length - 4);
+ }
file.data.uid = getRequestUid(pathname);
_.each(_.get(file, 'data.variables', []), (variable) => (variable.uid = uuid()));
@@ -128,7 +131,10 @@ const changeEnvironmentFile = async (win, pathname, collectionUid, collectionPat
const bruContent = fs.readFileSync(pathname, 'utf8');
file.data = bruToEnvJson(bruContent);
- file.data.name = basename.substring(0, basename.length - 4);
+ // Older env files do not have a meta block
+ if (!file.data.name) {
+ file.data.name = basename.substring(0, basename.length - 4);
+ }
file.data.uid = getRequestUid(pathname);
_.each(_.get(file, 'data.variables', []), (variable) => (variable.uid = uuid()));
@@ -143,6 +149,8 @@ const changeEnvironmentFile = async (win, pathname, collectionUid, collectionPat
});
}
+ console.log(file);
+
// we are reusing the addEnvironmentFile event itself
// this is because the uid of the pathname remains the same
// and the collection tree will be able to update the existing environment
diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js
index 98ff0cd41f..135c7049f6 100644
--- a/packages/bruno-electron/src/ipc/collection.js
+++ b/packages/bruno-electron/src/ipc/collection.js
@@ -2,7 +2,7 @@ const _ = require('lodash');
const fs = require('fs');
const path = require('path');
const { ipcMain, shell } = require('electron');
-const { envJsonToBru, bruToJson, jsonToBru } = require('../bru');
+const { envJsonToBru, bruToJson, jsonToBru, bruToEnvJson } = require('../bru');
const {
writeFile,
@@ -11,7 +11,8 @@ const {
browseDirectory,
createDirectory,
searchForBruFiles,
- sanitizeDirectoryName
+ sanitizeDirectoryName,
+ sanitizeFilenme
} = require('../utils/filesystem');
const { stringifyJson } = require('../utils/common');
const { openCollectionDialog, openCollection } = require('../app/collections');
@@ -99,12 +100,14 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// new request
ipcMain.handle('renderer:new-request', async (event, pathname, request) => {
try {
- if (fs.existsSync(pathname)) {
- throw new Error(`path: ${pathname} already exists`);
+ const sanitizedPathname = path.join(pathname, sanitizeFilenme(request.name) + '.bru');
+
+ if (fs.existsSync(sanitizedPathname)) {
+ throw new Error(`path: ${sanitizedPathname} already exists`);
}
const content = jsonToBru(request);
- await writeFile(pathname, content);
+ await writeFile(sanitizedPathname, content);
} catch (error) {
return Promise.reject(error);
}
@@ -132,13 +135,15 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
await createDirectory(envDirPath);
}
- const envFilePath = path.join(envDirPath, `${name}.bru`);
+ const filenameSanatized = `${sanitizeFilenme(name)}.bru`;
+ const envFilePath = path.join(envDirPath, filenameSanatized);
if (fs.existsSync(envFilePath)) {
throw new Error(`environment: ${envFilePath} already exists`);
}
const content = envJsonToBru({
- variables: []
+ variables: [],
+ name: name
});
await writeFile(envFilePath, content);
} catch (error) {
@@ -154,13 +159,15 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
await createDirectory(envDirPath);
}
- const envFilePath = path.join(envDirPath, `${name}.bru`);
+ const filenameSanatized = sanitizeFilenme(`${name}.bru`);
+ const envFilePath = path.join(envDirPath, filenameSanatized);
if (fs.existsSync(envFilePath)) {
throw new Error(`environment: ${envFilePath} already exists`);
}
const content = envJsonToBru({
- variables: baseVariables
+ variables: baseVariables,
+ name: name
});
await writeFile(envFilePath, content);
} catch (error) {
@@ -176,9 +183,13 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
await createDirectory(envDirPath);
}
- const envFilePath = path.join(envDirPath, `${environment.name}.bru`);
+ let envFilePath = path.join(envDirPath, `${sanitizeFilenme(environment.name)}.bru`);
if (!fs.existsSync(envFilePath)) {
- throw new Error(`environment: ${envFilePath} does not exist`);
+ // Fallback to unsatized filename for old envs
+ envFilePath = path.join(envDirPath, `${environment.name}.bru`);
+ if (!fs.existsSync(envFilePath)) {
+ throw new Error(`environment: ${envFilePath} does not exist`);
+ }
}
if (envHasSecrets(environment)) {
@@ -196,16 +207,26 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:rename-environment', async (event, collectionPathname, environmentName, newName) => {
try {
const envDirPath = path.join(collectionPathname, 'environments');
- const envFilePath = path.join(envDirPath, `${environmentName}.bru`);
+ let envFilePath = path.join(envDirPath, `${sanitizeFilenme(environmentName)}.bru`);
if (!fs.existsSync(envFilePath)) {
- throw new Error(`environment: ${envFilePath} does not exist`);
+ // Fallback to unsatized env name
+ envFilePath = path.join(envDirPath, `${environmentName}.bru`);
+ if (!fs.existsSync(envFilePath)) {
+ throw new Error(`environment: ${envFilePath} does not exist`);
+ }
}
- const newEnvFilePath = path.join(envDirPath, `${newName}.bru`);
- if (fs.existsSync(newEnvFilePath)) {
+ const newEnvFilePath = path.join(envDirPath, `${sanitizeFilenme(newName)}.bru`);
+ if (fs.existsSync(newEnvFilePath) && envFilePath !== newEnvFilePath) {
throw new Error(`environment: ${newEnvFilePath} already exists`);
}
+ // Update the name in the environment meta
+ const bruContent = fs.readFileSync(envFilePath, 'utf8');
+ const content = bruToEnvJson(bruContent);
+ content.name = newName;
+ await writeFile(envFilePath, envJsonToBru(content));
+
fs.renameSync(envFilePath, newEnvFilePath);
environmentSecretsStore.renameEnvironment(collectionPathname, environmentName, newName);
@@ -218,9 +239,13 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:delete-environment', async (event, collectionPathname, environmentName) => {
try {
const envDirPath = path.join(collectionPathname, 'environments');
- const envFilePath = path.join(envDirPath, `${environmentName}.bru`);
+ let envFilePath = path.join(envDirPath, `${sanitizeFilenme(environmentName)}.bru`);
if (!fs.existsSync(envFilePath)) {
- throw new Error(`environment: ${envFilePath} does not exist`);
+ // Fallback to unsatized env name
+ envFilePath = path.join(envDirPath, `${environmentName}.bru`);
+ if (!fs.existsSync(envFilePath)) {
+ throw new Error(`environment: ${envFilePath} does not exist`);
+ }
}
fs.unlinkSync(envFilePath);
@@ -232,42 +257,50 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
});
// rename item
- ipcMain.handle('renderer:rename-item', async (event, oldPath, newPath, newName) => {
+ ipcMain.handle('renderer:rename-item', async (event, oldPathFull, newPath, newName) => {
try {
- if (!fs.existsSync(oldPath)) {
- throw new Error(`path: ${oldPath} does not exist`);
- }
- if (fs.existsSync(newPath)) {
- throw new Error(`path: ${oldPath} already exists`);
+ if (!fs.existsSync(oldPathFull)) {
+ throw new Error(`path: ${oldPathFull} does not exist`);
}
// if its directory, rename and return
- if (isDirectory(oldPath)) {
- const bruFilesAtSource = await searchForBruFiles(oldPath);
+ if (isDirectory(oldPathFull)) {
+ const bruFilesAtSource = await searchForBruFiles(oldPathFull);
+
+ const newPathFull = path.join(newPath, newName);
+ if (fs.existsSync(newPathFull)) {
+ throw new Error(`path: ${newPathFull} already exists`);
+ }
for (let bruFile of bruFilesAtSource) {
- const newBruFilePath = bruFile.replace(oldPath, newPath);
+ const newBruFilePath = bruFile.replace(oldPathFull, newPathFull);
moveRequestUid(bruFile, newBruFilePath);
}
- return fs.renameSync(oldPath, newPath);
+ return fs.renameSync(oldPathFull, newPathFull);
}
- const isBru = hasBruExtension(oldPath);
+ const isBru = hasBruExtension(oldPathFull);
if (!isBru) {
- throw new Error(`path: ${oldPath} is not a bru file`);
+ throw new Error(`path: ${oldPathFull} is not a bru file`);
}
+ const newSantitizedPath = path.join(newPath, sanitizeFilenme(newName) + '.bru');
+
// update name in file and save new copy, then delete old copy
- const data = fs.readFileSync(oldPath, 'utf8');
+ const data = fs.readFileSync(oldPathFull, 'utf8');
const jsonData = bruToJson(data);
jsonData.name = newName;
- moveRequestUid(oldPath, newPath);
+ moveRequestUid(oldPathFull, newSantitizedPath);
const content = jsonToBru(jsonData);
- await writeFile(newPath, content);
- await fs.unlinkSync(oldPath);
+ await writeFile(newSantitizedPath, content);
+
+ // Because of santization the name can change but the path stays the same
+ if (newSantitizedPath !== oldPathFull) {
+ fs.unlinkSync(oldPathFull);
+ }
} catch (error) {
return Promise.reject(error);
}
@@ -290,6 +323,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:delete-item', async (event, pathname, type) => {
try {
if (type === 'folder') {
+ console.log(pathname);
if (!fs.existsSync(pathname)) {
return Promise.reject(new Error('The directory does not exist'));
}
diff --git a/packages/bruno-electron/src/utils/filesystem.js b/packages/bruno-electron/src/utils/filesystem.js
index b55dfd7258..00834cfecd 100644
--- a/packages/bruno-electron/src/utils/filesystem.js
+++ b/packages/bruno-electron/src/utils/filesystem.js
@@ -118,6 +118,10 @@ const sanitizeDirectoryName = (name) => {
return name.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '-');
};
+const sanitizeFilenme = (name) => {
+ return name.replace(/[^\w-_.]/g, '_');
+};
+
module.exports = {
isValidPathname,
exists,
@@ -132,5 +136,6 @@ module.exports = {
browseDirectory,
searchForFiles,
searchForBruFiles,
- sanitizeDirectoryName
+ sanitizeDirectoryName,
+ sanitizeFilenme
};
diff --git a/packages/bruno-lang/v2/src/envToJson.js b/packages/bruno-lang/v2/src/envToJson.js
index eef4de375d..5eb69e9293 100644
--- a/packages/bruno-lang/v2/src/envToJson.js
+++ b/packages/bruno-lang/v2/src/envToJson.js
@@ -2,7 +2,7 @@ const ohm = require('ohm-js');
const _ = require('lodash');
const grammar = ohm.grammar(`Bru {
- BruEnvFile = (vars | secretvars)*
+ BruEnvFile = (meta | vars | secretvars)*
nl = "\\r"? "\\n"
st = " " | "\\t"
@@ -25,6 +25,8 @@ const grammar = ohm.grammar(`Bru {
arrayvalue = arrayvaluechar*
arrayvaluechar = ~(nl | st | "[" | "]" | ",") any
+ meta = "meta" dictionary
+
secretvars = "vars:secret" array
vars = "vars" dictionary
}`);
@@ -74,6 +76,14 @@ const mapArrayListToKeyValPairs = (arrayList = []) => {
});
};
+const mapPairListToKeyValPair = (pairList = []) => {
+ if (!pairList || !pairList.length) {
+ return {};
+ }
+
+ return _.merge({}, ...pairList[0]);
+};
+
const concatArrays = (objValue, srcValue) => {
if (_.isArray(objValue) && _.isArray(srcValue)) {
return objValue.concat(srcValue);
@@ -134,6 +144,13 @@ const sem = grammar.createSemantics().addAttribute('ast', {
_iter(...elements) {
return elements.map((e) => e.ast);
},
+ meta(_1, dictionary) {
+ let meta = mapPairListToKeyValPair(dictionary.ast);
+
+ return {
+ name: meta.name
+ };
+ },
vars(_1, dictionary) {
const vars = mapPairListToKeyValPairs(dictionary.ast);
_.each(vars, (v) => {
diff --git a/packages/bruno-lang/v2/src/jsonToEnv.js b/packages/bruno-lang/v2/src/jsonToEnv.js
index 42d0a4281d..d339fa71e0 100644
--- a/packages/bruno-lang/v2/src/jsonToEnv.js
+++ b/packages/bruno-lang/v2/src/jsonToEnv.js
@@ -1,6 +1,10 @@
const _ = require('lodash');
const envToJson = (json) => {
+ const meta = `meta {
+ name: ${json.name}
+}\n\n`;
+
const variables = _.get(json, 'variables', []);
const vars = variables
.filter((variable) => !variable.secret)
@@ -19,12 +23,12 @@ const envToJson = (json) => {
});
if (!variables || !variables.length) {
- return `vars {
+ return `${meta}vars {
}
`;
}
- let output = '';
+ let output = meta;
if (vars.length) {
output += `vars {
${vars.join('\n')}
From c7d0135fa04aee7573f005cafa04c293c4a5560b Mon Sep 17 00:00:00 2001
From: Its-treason <39559178+Its-treason@users.noreply.github.com>
Date: Sun, 8 Oct 2023 15:09:56 +0200
Subject: [PATCH 003/209] fix: Fix some bugs with the new name validation
---
.../ResponsePane/QueryResult/ImagePreview.js | 27 +++++++++++++++++++
.../providers/App/useCollectionNextAction.js | 1 +
.../ReduxStore/slices/collections/actions.js | 12 ++++-----
.../ReduxStore/slices/collections/index.js | 1 -
.../bruno-app/src/utils/collections/index.js | 2 ++
packages/bruno-app/src/utils/common/index.js | 4 +++
packages/bruno-electron/src/app/watcher.js | 2 --
packages/bruno-electron/src/ipc/collection.js | 5 +++-
8 files changed, 44 insertions(+), 10 deletions(-)
create mode 100644 packages/bruno-app/src/components/ResponsePane/QueryResult/ImagePreview.js
diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/ImagePreview.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/ImagePreview.js
new file mode 100644
index 0000000000..6a1819082c
--- /dev/null
+++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/ImagePreview.js
@@ -0,0 +1,27 @@
+import { useRef } from 'react';
+import { useEffect } from 'react';
+
+const ImagePreview = ({ data, contentType }) => {
+ const imgRef = useRef(null);
+
+ useEffect(() => {
+ imgRef.current.src = 'data:image/png;base64,' + Buffer.from(data).toString('base64');
+ // const blob = new Blob([encodeURIComponent(data)], {
+ // type: contentType,
+ // })
+ // var reader = new FileReader();
+ // reader.onloadend = function () {
+ // console.log(reader.result, reader.result.length)
+ // imgRef.current.src = reader.result;
+ // };
+ // reader.readAsDataURL(blob);
+ }, [data]);
+
+ return (
+
+
+
+ );
+};
+
+export default ImagePreview;
diff --git a/packages/bruno-app/src/providers/App/useCollectionNextAction.js b/packages/bruno-app/src/providers/App/useCollectionNextAction.js
index 94c58f604d..5399ae365c 100644
--- a/packages/bruno-app/src/providers/App/useCollectionNextAction.js
+++ b/packages/bruno-app/src/providers/App/useCollectionNextAction.js
@@ -14,6 +14,7 @@ const useCollectionNextAction = () => {
useEffect(() => {
each(collections, (collection) => {
if (collection.nextAction && collection.nextAction.type === 'OPEN_REQUEST') {
+ console.log(collection.nextAction.payload.pathname, JSON.stringify(collection));
const item = findItemInCollectionByPathname(collection, get(collection, 'nextAction.payload.pathname'));
if (item) {
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 ed9b81e0de..f4efe51b3f 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
@@ -1,4 +1,3 @@
-import path from 'path';
import toast from 'react-hot-toast';
import trim from 'lodash/trim';
import find from 'lodash/find';
@@ -45,8 +44,10 @@ import {
import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs';
import { resolveRequestFilename } from 'utils/common/platform';
+import { sanitizeFilenme } from 'utils/common/index';
+import os from 'os';
-const PATH_SEPARATOR = path.sep;
+const PATH_SEPARATOR = /Windows/i.test(os.release()) ? '\\' : '/';
export const renameCollection = (newName, collectionUid) => (dispatch, getState) => {
const state = getState();
@@ -250,7 +251,6 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta
if (!collection) {
return reject(new Error('Collection not found'));
}
- console.log(collection);
const collectionCopy = cloneDeep(collection);
const item = findItemInCollection(collectionCopy, itemUid);
if (!item) {
@@ -584,7 +584,7 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
if (!reqWithSameNameExists) {
const { ipcRenderer } = window;
- ipcRenderer.invoke('renderer:new-request', fullName, item).then(resolve).catch(reject);
+ ipcRenderer.invoke('renderer:new-request', collection.pathname, item).then(resolve).catch(reject);
// the useCollectionNextAction() will track this and open the new request in a new tab
// once the request is created
dispatch(
@@ -592,7 +592,7 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
nextAction: {
type: 'OPEN_REQUEST',
payload: {
- pathname: fullName
+ pathname: collection.pathname + PATH_SEPARATOR + sanitizeFilenme(item.name) + '.bru'
}
},
collectionUid
@@ -622,7 +622,7 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
nextAction: {
type: 'OPEN_REQUEST',
payload: {
- pathname: fullName
+ pathname: collection.pathname + PATH_SEPARATOR + sanitizeFilenme(item.name) + '.bru'
}
},
collectionUid
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 402236846e..21647aea2e 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
@@ -979,7 +979,6 @@ export const collectionsSlice = createSlice({
}
},
collectionAddDirectoryEvent: (state, action) => {
- console.log('ADD DIR', action.payload);
const { dir } = action.payload;
const collection = findCollectionByUid(state.collections, dir.meta.collectionUid);
diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js
index 9eb6c0d1f7..f1b452782e 100644
--- a/packages/bruno-app/src/utils/collections/index.js
+++ b/packages/bruno-app/src/utils/collections/index.js
@@ -98,6 +98,8 @@ export const findItemByPathname = (items = [], pathname) => {
export const findItemInCollectionByPathname = (collection, pathname) => {
let flattenedItems = flattenItems(collection.items);
+ console.log(flattenItems);
+ each(flattenItems, (item) => console.log(item.pathname, pathname, item.pathname === pathname));
return findItemByPathname(flattenedItems, pathname);
};
diff --git a/packages/bruno-app/src/utils/common/index.js b/packages/bruno-app/src/utils/common/index.js
index 992ec233e6..d49689e105 100644
--- a/packages/bruno-app/src/utils/common/index.js
+++ b/packages/bruno-app/src/utils/common/index.js
@@ -94,3 +94,7 @@ export const getContentType = (headers) => {
return '';
};
+
+export const sanitizeFilenme = (name) => {
+ return name.replace(/[^\w-_.]/g, '_');
+};
diff --git a/packages/bruno-electron/src/app/watcher.js b/packages/bruno-electron/src/app/watcher.js
index c6c377958e..64e30fba23 100644
--- a/packages/bruno-electron/src/app/watcher.js
+++ b/packages/bruno-electron/src/app/watcher.js
@@ -142,8 +142,6 @@ const changeEnvironmentFile = async (win, pathname, collectionUid, collectionPat
});
}
- console.log(file);
-
// we are reusing the addEnvironmentFile event itself
// this is because the uid of the pathname remains the same
// and the collection tree will be able to update the existing environment
diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js
index 135c7049f6..485e9f5594 100644
--- a/packages/bruno-electron/src/ipc/collection.js
+++ b/packages/bruno-electron/src/ipc/collection.js
@@ -285,6 +285,10 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
const newSantitizedPath = path.join(newPath, sanitizeFilenme(newName) + '.bru');
+ console.log(oldPathFull, newSantitizedPath);
+ if (fs.existsSync(newSantitizedPath) && newSantitizedPath !== oldPathFull) {
+ throw new Error(`path: ${newSantitizedPath} already exists`);
+ }
// update name in file and save new copy, then delete old copy
const data = fs.readFileSync(oldPathFull, 'utf8');
@@ -323,7 +327,6 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:delete-item', async (event, pathname, type) => {
try {
if (type === 'folder') {
- console.log(pathname);
if (!fs.existsSync(pathname)) {
return Promise.reject(new Error('The directory does not exist'));
}
From ab05ddcb04190f2dad51bad72241e31d74a9b4e5 Mon Sep 17 00:00:00 2001
From: Its-treason <39559178+Its-treason@users.noreply.github.com>
Date: Sun, 8 Oct 2023 15:32:07 +0200
Subject: [PATCH 004/209] test: Update bruno-lang v2 Env file tests
---
.../bruno-lang/v2/tests/envToJson.spec.js | 36 ++++++++++++-
.../bruno-lang/v2/tests/jsonToEnv.spec.js | 54 ++++++++++++++-----
2 files changed, 77 insertions(+), 13 deletions(-)
diff --git a/packages/bruno-lang/v2/tests/envToJson.spec.js b/packages/bruno-lang/v2/tests/envToJson.spec.js
index fbb74f2b95..e3dcebf0dc 100644
--- a/packages/bruno-lang/v2/tests/envToJson.spec.js
+++ b/packages/bruno-lang/v2/tests/envToJson.spec.js
@@ -14,8 +14,30 @@ vars {
expect(output).toEqual(expected);
});
+ it('should parse empty vars with meta', () => {
+ const input = `
+meta {
+ name: This is a test
+}
+
+vars {
+}`;
+
+ const output = parser(input);
+ const expected = {
+ variables: [],
+ name: 'This is a test'
+ };
+
+ expect(output).toEqual(expected);
+ });
+
it('should parse single var line', () => {
const input = `
+meta {
+ name: This is a test
+}
+
vars {
url: http://localhost:3000
}`;
@@ -29,7 +51,8 @@ vars {
enabled: true,
secret: false
}
- ]
+ ],
+ name: 'This is a test'
};
expect(output).toEqual(expected);
@@ -37,6 +60,10 @@ vars {
it('should parse multiple var lines', () => {
const input = `
+meta {
+ name: This is a test
+}
+
vars {
url: http://localhost:3000
port: 3000
@@ -45,6 +72,7 @@ vars {
const output = parser(input);
const expected = {
+ name: 'This is a test',
variables: [
{
name: 'url',
@@ -73,6 +101,11 @@ vars {
it('should gracefully handle empty lines and spaces', () => {
const input = `
+meta {
+ name: Yet another test
+ other-thing: ignore me
+}
+
vars {
url: http://localhost:3000
port: 3000
@@ -82,6 +115,7 @@ vars {
const output = parser(input);
const expected = {
+ name: 'Yet another test',
variables: [
{
name: 'url',
diff --git a/packages/bruno-lang/v2/tests/jsonToEnv.spec.js b/packages/bruno-lang/v2/tests/jsonToEnv.spec.js
index 62b7aa2697..9c32cdc112 100644
--- a/packages/bruno-lang/v2/tests/jsonToEnv.spec.js
+++ b/packages/bruno-lang/v2/tests/jsonToEnv.spec.js
@@ -3,11 +3,16 @@ const parser = require('../src/jsonToEnv');
describe('env parser', () => {
it('should parse empty vars', () => {
const input = {
- variables: []
+ variables: [],
+ name: 'test'
};
const output = parser(input);
- const expected = `vars {
+ const expected = `meta {
+ name: test
+}
+
+vars {
}
`;
@@ -22,11 +27,16 @@ describe('env parser', () => {
value: 'http://localhost:3000',
enabled: true
}
- ]
+ ],
+ name: 'test'
};
const output = parser(input);
- const expected = `vars {
+ const expected = `meta {
+ name: test
+}
+
+vars {
url: http://localhost:3000
}
`;
@@ -46,10 +56,15 @@ describe('env parser', () => {
value: '3000',
enabled: false
}
- ]
+ ],
+ name: 'test'
};
- const expected = `vars {
+ const expected = `meta {
+ name: test
+}
+
+vars {
url: http://localhost:3000
~port: 3000
}
@@ -72,11 +87,16 @@ describe('env parser', () => {
enabled: true,
secret: true
}
- ]
+ ],
+ name: 'test'
};
const output = parser(input);
- const expected = `vars {
+ const expected = `meta {
+ name: test
+}
+
+vars {
url: http://localhost:3000
}
vars:secret [
@@ -106,11 +126,16 @@ vars:secret [
enabled: false,
secret: true
}
- ]
+ ],
+ name: 'test'
};
const output = parser(input);
- const expected = `vars {
+ const expected = `meta {
+ name: test
+}
+
+vars {
url: http://localhost:3000
}
vars:secret [
@@ -130,11 +155,16 @@ vars:secret [
enabled: true,
secret: true
}
- ]
+ ],
+ name: 'test'
};
const output = parser(input);
- const expected = `vars:secret [
+ const expected = `meta {
+ name: test
+}
+
+vars:secret [
token
]
`;
From 931259825d348c745223547769424dd707f4b248 Mon Sep 17 00:00:00 2001
From: Its-treason <39559178+Its-treason@users.noreply.github.com>
Date: Sun, 8 Oct 2023 17:42:51 +0200
Subject: [PATCH 005/209] fix: Cleanup pr #349
---
.../ResponsePane/QueryResult/ImagePreview.js | 27 -------------------
.../components/Sidebar/NewRequest/index.js | 1 -
.../providers/App/useCollectionNextAction.js | 1 -
.../bruno-app/src/utils/collections/index.js | 2 --
packages/bruno-electron/src/ipc/collection.js | 1 -
5 files changed, 32 deletions(-)
delete mode 100644 packages/bruno-app/src/components/ResponsePane/QueryResult/ImagePreview.js
diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/ImagePreview.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/ImagePreview.js
deleted file mode 100644
index 6a1819082c..0000000000
--- a/packages/bruno-app/src/components/ResponsePane/QueryResult/ImagePreview.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { useRef } from 'react';
-import { useEffect } from 'react';
-
-const ImagePreview = ({ data, contentType }) => {
- const imgRef = useRef(null);
-
- useEffect(() => {
- imgRef.current.src = 'data:image/png;base64,' + Buffer.from(data).toString('base64');
- // const blob = new Blob([encodeURIComponent(data)], {
- // type: contentType,
- // })
- // var reader = new FileReader();
- // reader.onloadend = function () {
- // console.log(reader.result, reader.result.length)
- // imgRef.current.src = reader.result;
- // };
- // reader.readAsDataURL(blob);
- }, [data]);
-
- return (
-
-
-
- );
-};
-
-export default ImagePreview;
diff --git a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js
index 241a9d3676..6a753fd97b 100644
--- a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js
+++ b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js
@@ -11,7 +11,6 @@ import { addTab } from 'providers/ReduxStore/slices/tabs';
import HttpMethodSelector from 'components/RequestPane/QueryUrl/HttpMethodSelector';
import { getDefaultRequestPaneTab } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
-import { filenameRegex } from 'utils/common/regex';
const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
const dispatch = useDispatch();
diff --git a/packages/bruno-app/src/providers/App/useCollectionNextAction.js b/packages/bruno-app/src/providers/App/useCollectionNextAction.js
index 5399ae365c..94c58f604d 100644
--- a/packages/bruno-app/src/providers/App/useCollectionNextAction.js
+++ b/packages/bruno-app/src/providers/App/useCollectionNextAction.js
@@ -14,7 +14,6 @@ const useCollectionNextAction = () => {
useEffect(() => {
each(collections, (collection) => {
if (collection.nextAction && collection.nextAction.type === 'OPEN_REQUEST') {
- console.log(collection.nextAction.payload.pathname, JSON.stringify(collection));
const item = findItemInCollectionByPathname(collection, get(collection, 'nextAction.payload.pathname'));
if (item) {
diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js
index f1b452782e..9eb6c0d1f7 100644
--- a/packages/bruno-app/src/utils/collections/index.js
+++ b/packages/bruno-app/src/utils/collections/index.js
@@ -98,8 +98,6 @@ export const findItemByPathname = (items = [], pathname) => {
export const findItemInCollectionByPathname = (collection, pathname) => {
let flattenedItems = flattenItems(collection.items);
- console.log(flattenItems);
- each(flattenItems, (item) => console.log(item.pathname, pathname, item.pathname === pathname));
return findItemByPathname(flattenedItems, pathname);
};
diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js
index c1c0edd5e5..84ce2eea86 100644
--- a/packages/bruno-electron/src/ipc/collection.js
+++ b/packages/bruno-electron/src/ipc/collection.js
@@ -285,7 +285,6 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
const newSantitizedPath = path.join(newPath, sanitizeFilenme(newName) + '.bru');
- console.log(oldPathFull, newSantitizedPath);
if (fs.existsSync(newSantitizedPath) && newSantitizedPath !== oldPathFull) {
throw new Error(`path: ${newSantitizedPath} already exists`);
}
From 981d9d69af6d27768a10573fb5d32204c5ca84c6 Mon Sep 17 00:00:00 2001
From: Kavinkumar
Date: Wed, 11 Oct 2023 16:13:43 +0530
Subject: [PATCH 006/209] Fixed application crash on renaming request names by
changing cases
---
packages/bruno-electron/src/ipc/collection.js | 18 +++++++++++++++---
1 file changed, 15 insertions(+), 3 deletions(-)
diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js
index 944a04f01d..f639bf4d32 100644
--- a/packages/bruno-electron/src/ipc/collection.js
+++ b/packages/bruno-electron/src/ipc/collection.js
@@ -250,10 +250,11 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// rename item
ipcMain.handle('renderer:rename-item', async (event, oldPath, newPath, newName) => {
try {
- if (!fs.existsSync(oldPath)) {
+ // the file existing is checked with case insensitive file name. I want to check with case sensitive
+ if (!fileExistsWithCase(oldPath)) {
throw new Error(`path: ${oldPath} does not exist`);
}
- if (fs.existsSync(newPath)) {
+ if (fileExistsWithCase(newPath)) {
throw new Error(`path: ${oldPath} already exists`);
}
@@ -282,8 +283,8 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
moveRequestUid(oldPath, newPath);
const content = jsonToBru(jsonData);
- await writeFile(newPath, content);
await fs.unlinkSync(oldPath);
+ await writeFile(newPath, content);
} catch (error) {
return Promise.reject(error);
}
@@ -526,4 +527,15 @@ const registerCollectionsIpc = (mainWindow, watcher, lastOpenedCollections) => {
registerMainEventHandlers(mainWindow, watcher, lastOpenedCollections);
};
+function fileExistsWithCase(filePath) {
+ try {
+ const folderPath = path.dirname(filePath);
+ const fileName = path.basename(filePath);
+ const files = fs.readdirSync(folderPath);
+ return files.includes(fileName);
+ } catch (error) {
+ return false;
+ }
+}
+
module.exports = registerCollectionsIpc;
From 3d8edf39983b20c1acebb3aab135b6651a282335 Mon Sep 17 00:00:00 2001
From: Kavinkumar
Date: Fri, 13 Oct 2023 11:05:35 +0530
Subject: [PATCH 007/209] Fix :: error on renaming environment names while
changing cases
---
packages/bruno-electron/src/ipc/collection.js | 44 +++++++++----------
1 file changed, 22 insertions(+), 22 deletions(-)
diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js
index f639bf4d32..d69c393c81 100644
--- a/packages/bruno-electron/src/ipc/collection.js
+++ b/packages/bruno-electron/src/ipc/collection.js
@@ -47,7 +47,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
async (event, collectionName, collectionFolderName, collectionLocation) => {
try {
const dirPath = path.join(collectionLocation, collectionFolderName);
- if (fs.existsSync(dirPath)) {
+ if (fileExists(dirPath)) {
throw new Error(`collection: ${dirPath} already exists`);
}
@@ -115,7 +115,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// new request
ipcMain.handle('renderer:new-request', async (event, pathname, request) => {
try {
- if (fs.existsSync(pathname)) {
+ if (fileExists(pathname)) {
throw new Error(`path: ${pathname} already exists`);
}
@@ -129,7 +129,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// save request
ipcMain.handle('renderer:save-request', async (event, pathname, request) => {
try {
- if (!fs.existsSync(pathname)) {
+ if (!fileExists(pathname)) {
throw new Error(`path: ${pathname} does not exist`);
}
@@ -144,12 +144,12 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:create-environment', async (event, collectionPathname, name) => {
try {
const envDirPath = path.join(collectionPathname, 'environments');
- if (!fs.existsSync(envDirPath)) {
+ if (!fileExists(envDirPath)) {
await createDirectory(envDirPath);
}
const envFilePath = path.join(envDirPath, `${name}.bru`);
- if (fs.existsSync(envFilePath)) {
+ if (fileExists(envFilePath)) {
throw new Error(`environment: ${envFilePath} already exists`);
}
@@ -166,12 +166,12 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:copy-environment', async (event, collectionPathname, name, baseVariables) => {
try {
const envDirPath = path.join(collectionPathname, 'environments');
- if (!fs.existsSync(envDirPath)) {
+ if (!fileExists(envDirPath)) {
await createDirectory(envDirPath);
}
const envFilePath = path.join(envDirPath, `${name}.bru`);
- if (fs.existsSync(envFilePath)) {
+ if (fileExists(envFilePath)) {
throw new Error(`environment: ${envFilePath} already exists`);
}
@@ -188,12 +188,12 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:save-environment', async (event, collectionPathname, environment) => {
try {
const envDirPath = path.join(collectionPathname, 'environments');
- if (!fs.existsSync(envDirPath)) {
+ if (!fileExists(envDirPath)) {
await createDirectory(envDirPath);
}
const envFilePath = path.join(envDirPath, `${environment.name}.bru`);
- if (!fs.existsSync(envFilePath)) {
+ if (!fileExists(envFilePath)) {
throw new Error(`environment: ${envFilePath} does not exist`);
}
@@ -213,12 +213,12 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
try {
const envDirPath = path.join(collectionPathname, 'environments');
const envFilePath = path.join(envDirPath, `${environmentName}.bru`);
- if (!fs.existsSync(envFilePath)) {
+ if (!fileExists(envFilePath)) {
throw new Error(`environment: ${envFilePath} does not exist`);
}
const newEnvFilePath = path.join(envDirPath, `${newName}.bru`);
- if (fs.existsSync(newEnvFilePath)) {
+ if (fileExists(newEnvFilePath)) {
throw new Error(`environment: ${newEnvFilePath} already exists`);
}
@@ -235,7 +235,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
try {
const envDirPath = path.join(collectionPathname, 'environments');
const envFilePath = path.join(envDirPath, `${environmentName}.bru`);
- if (!fs.existsSync(envFilePath)) {
+ if (!fileExists(envFilePath)) {
throw new Error(`environment: ${envFilePath} does not exist`);
}
@@ -251,10 +251,10 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:rename-item', async (event, oldPath, newPath, newName) => {
try {
// the file existing is checked with case insensitive file name. I want to check with case sensitive
- if (!fileExistsWithCase(oldPath)) {
+ if (!fileExists(oldPath)) {
throw new Error(`path: ${oldPath} does not exist`);
}
- if (fileExistsWithCase(newPath)) {
+ if (fileExists(newPath)) {
throw new Error(`path: ${oldPath} already exists`);
}
@@ -293,7 +293,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// new folder
ipcMain.handle('renderer:new-folder', async (event, pathname) => {
try {
- if (!fs.existsSync(pathname)) {
+ if (!fileExists(pathname)) {
fs.mkdirSync(pathname);
} else {
return Promise.reject(new Error('The directory already exists'));
@@ -307,7 +307,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:delete-item', async (event, pathname, type) => {
try {
if (type === 'folder') {
- if (!fs.existsSync(pathname)) {
+ if (!fileExists(pathname)) {
return Promise.reject(new Error('The directory does not exist'));
}
@@ -319,7 +319,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
fs.rmSync(pathname, { recursive: true, force: true });
} else if (['http-request', 'graphql-request'].includes(type)) {
- if (!fs.existsSync(pathname)) {
+ if (!fileExists(pathname)) {
return Promise.reject(new Error('The file does not exist'));
}
@@ -353,7 +353,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
let collectionName = sanitizeDirectoryName(collection.name);
let collectionPath = path.join(collectionLocation, collectionName);
- if (fs.existsSync(collectionPath)) {
+ if (fileExists(collectionPath)) {
throw new Error(`collection: ${collectionPath} already exists`);
}
@@ -378,7 +378,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
const parseEnvironments = (environments = [], collectionPath) => {
const envDirPath = path.join(collectionPath, 'environments');
- if (!fs.existsSync(envDirPath)) {
+ if (!fileExists(envDirPath)) {
fs.mkdirSync(envDirPath);
}
@@ -449,11 +449,11 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
const folderName = path.basename(folderPath);
const newFolderPath = path.join(destinationPath, folderName);
- if (!fs.existsSync(folderPath)) {
+ if (!fileExists(folderPath)) {
throw new Error(`folder: ${folderPath} does not exist`);
}
- if (fs.existsSync(newFolderPath)) {
+ if (fileExists(newFolderPath)) {
throw new Error(`folder: ${newFolderPath} already exists`);
}
@@ -527,7 +527,7 @@ const registerCollectionsIpc = (mainWindow, watcher, lastOpenedCollections) => {
registerMainEventHandlers(mainWindow, watcher, lastOpenedCollections);
};
-function fileExistsWithCase(filePath) {
+function fileExists(filePath) {
try {
const folderPath = path.dirname(filePath);
const fileName = path.basename(filePath);
From 0a11a29b75fe01b99f13b03395d4536ed1e23c90 Mon Sep 17 00:00:00 2001
From: Its-treason <39559178+Its-treason@users.noreply.github.com>
Date: Fri, 13 Oct 2023 23:48:20 +0200
Subject: [PATCH 008/209] Revert "fix(#251, #265): phantoms folders fix on
rename/delete needs to be run only on windows"
This reverts commit fcc12fb089472716328054665d767d7b0ae48e04.
---
.../ReduxStore/slices/collections/actions.js | 18 +++---------------
.../bruno-app/src/utils/common/platform.js | 1 -
2 files changed, 3 insertions(+), 16 deletions(-)
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 80c823454b..adbe776e91 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
@@ -21,7 +21,7 @@ import {
} from 'utils/collections';
import { collectionSchema, itemSchema, environmentSchema, environmentsSchema } from '@usebruno/schema';
import { waitForNextTick } from 'utils/common';
-import { getDirectoryName, isWindowsOS } from 'utils/common/platform';
+import { getDirectoryName } from 'utils/common/platform';
import { sendNetworkRequest, cancelNetworkRequest } from 'utils/network';
import {
@@ -311,13 +311,7 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta
ipcRenderer
.invoke('renderer:rename-item', item.pathname, newPathname, newName)
.then(() => {
- // In case of Mac and Linux, we get the unlinkDir and addDir IPC events from electron which takes care of updating the state
- // But in windows we don't get those events, so we need to update the state manually
- // This looks like an issue in our watcher library chokidar
- // GH: https://github.com/usebruno/bruno/issues/251
- if (isWindowsOS()) {
- dispatch(_renameItem({ newName, itemUid, collectionUid }));
- }
+ dispatch(_renameItem({ newName, itemUid, collectionUid }));
resolve();
})
.catch(reject);
@@ -405,13 +399,7 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => {
ipcRenderer
.invoke('renderer:delete-item', item.pathname, item.type)
.then(() => {
- // In case of Mac and Linux, we get the unlinkDir IPC event from electron which takes care of updating the state
- // But in windows we don't get those events, so we need to update the state manually
- // This looks like an issue in our watcher library chokidar
- // GH: https://github.com/usebruno/bruno/issues/265
- if (isWindowsOS()) {
- dispatch(_deleteItem({ itemUid, collectionUid }));
- }
+ dispatch(_deleteItem({ itemUid, collectionUid }));
resolve();
})
.catch((error) => reject(error));
diff --git a/packages/bruno-app/src/utils/common/platform.js b/packages/bruno-app/src/utils/common/platform.js
index 771daaf141..ceb84ad4b7 100644
--- a/packages/bruno-app/src/utils/common/platform.js
+++ b/packages/bruno-app/src/utils/common/platform.js
@@ -1,7 +1,6 @@
import trim from 'lodash/trim';
import path from 'path';
import slash from './slash';
-import platform from 'platform';
export const isElectron = () => {
if (!window) {
From 284fe463cb76e6d89313f77e64ac062123235bce Mon Sep 17 00:00:00 2001
From: Sahilm416 <114013781+Sahilm416@users.noreply.github.com>
Date: Sat, 14 Oct 2023 12:09:57 +0530
Subject: [PATCH 009/209] Feat: added feature to close the active tab with
middle mouse click
---
.../src/components/RequestTabs/index.js | 58 ++++++++++++++++++-
1 file changed, 57 insertions(+), 1 deletion(-)
diff --git a/packages/bruno-app/src/components/RequestTabs/index.js b/packages/bruno-app/src/components/RequestTabs/index.js
index 3063771e86..856b8061e2 100644
--- a/packages/bruno-app/src/components/RequestTabs/index.js
+++ b/packages/bruno-app/src/components/RequestTabs/index.js
@@ -4,11 +4,15 @@ import filter from 'lodash/filter';
import classnames from 'classnames';
import { IconChevronRight, IconChevronLeft } from '@tabler/icons';
import { useSelector, useDispatch } from 'react-redux';
-import { focusTab } from 'providers/ReduxStore/slices/tabs';
+import { focusTab, closeTabs } from 'providers/ReduxStore/slices/tabs';
import NewRequest from 'components/Sidebar/NewRequest';
import CollectionToolBar from './CollectionToolBar';
import RequestTab from './RequestTab';
import StyledWrapper from './StyledWrapper';
+import ConfirmRequestClose from './RequestTab/ConfirmRequestClose/index';
+import { deleteRequestDraft } from 'providers/ReduxStore/slices/collections/index';
+import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
+import { findItemInCollection } from 'utils/collections/index';
const RequestTabs = () => {
const dispatch = useDispatch();
@@ -19,6 +23,8 @@ const RequestTabs = () => {
const collections = useSelector((state) => state.collections.collections);
const leftSidebarWidth = useSelector((state) => state.app.leftSidebarWidth);
const screenWidth = useSelector((state) => state.app.screenWidth);
+ const [showConfirmClose, setShowConfirmClose] = useState(false);
+ const [item, setItem] = useState(null);
const getTabClassname = (tab, index) => {
return classnames('request-tab select-none', {
@@ -35,6 +41,22 @@ const RequestTabs = () => {
);
};
+ const handleMouseUp = (e, tab) => {
+ const item = findItemInCollection(activeCollection, tab.uid);
+ setItem(item);
+ e.stopPropagation();
+ e.preventDefault();
+
+ if (e.button === 1) {
+ if (item?.draft) return setShowConfirmClose(true);
+ dispatch(
+ closeTabs({
+ tabUids: [tab.uid]
+ })
+ );
+ }
+ };
+
const createNewTab = () => setNewRequestModalOpen(true);
if (!activeTabUid) {
@@ -79,6 +101,39 @@ const RequestTabs = () => {
// Todo: Must support ephemeral requests
return (
+ {showConfirmClose && (
+ setShowConfirmClose(false)}
+ onCloseWithoutSave={() => {
+ dispatch(
+ deleteRequestDraft({
+ itemUid: item.uid,
+ collectionUid: activeCollection.uid
+ })
+ );
+ dispatch(
+ closeTabs({
+ tabUids: [activeTabUid]
+ })
+ );
+ setShowConfirmClose(false);
+ }}
+ onSaveAndClose={() => {
+ dispatch(saveRequest(item.uid, activeCollection.uid))
+ .then(() => {
+ dispatch(
+ closeTabs({
+ tabUids: [activeTabUid]
+ })
+ );
+ setShowConfirmClose(false);
+ })
+ .catch((err) => {
+ console.log('err', err);
+ });
+ }}
+ />
+ )}
{newRequestModalOpen && (
setNewRequestModalOpen(false)} />
)}
@@ -106,6 +161,7 @@ const RequestTabs = () => {
? collectionRequestTabs.map((tab, index) => {
return (
handleMouseUp(e, tab)}
key={tab.uid}
className={getTabClassname(tab, index)}
role="tab"
From b8a3975b97e1414841c09f10ee876e46ce0ef1c8 Mon Sep 17 00:00:00 2001
From: Sahilm416 <114013781+Sahilm416@users.noreply.github.com>
Date: Sat, 14 Oct 2023 12:26:47 +0530
Subject: [PATCH 010/209] fixed lil bug while closing tabs
---
packages/bruno-app/src/components/RequestTabs/index.js | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/packages/bruno-app/src/components/RequestTabs/index.js b/packages/bruno-app/src/components/RequestTabs/index.js
index 856b8061e2..4ba47d080d 100644
--- a/packages/bruno-app/src/components/RequestTabs/index.js
+++ b/packages/bruno-app/src/components/RequestTabs/index.js
@@ -25,7 +25,7 @@ const RequestTabs = () => {
const screenWidth = useSelector((state) => state.app.screenWidth);
const [showConfirmClose, setShowConfirmClose] = useState(false);
const [item, setItem] = useState(null);
-
+ const [tab, setTab] = useState(null);
const getTabClassname = (tab, index) => {
return classnames('request-tab select-none', {
active: tab.uid === activeTabUid,
@@ -44,6 +44,7 @@ const RequestTabs = () => {
const handleMouseUp = (e, tab) => {
const item = findItemInCollection(activeCollection, tab.uid);
setItem(item);
+ setTab(tab);
e.stopPropagation();
e.preventDefault();
@@ -113,7 +114,7 @@ const RequestTabs = () => {
);
dispatch(
closeTabs({
- tabUids: [activeTabUid]
+ tabUids: [tab.uid]
})
);
setShowConfirmClose(false);
@@ -123,7 +124,7 @@ const RequestTabs = () => {
.then(() => {
dispatch(
closeTabs({
- tabUids: [activeTabUid]
+ tabUids: [tab.uid]
})
);
setShowConfirmClose(false);
From 307c5d0177133877fed22551d266be002442a128 Mon Sep 17 00:00:00 2001
From: Its-treason <39559178+Its-treason@users.noreply.github.com>
Date: Sat, 14 Oct 2023 19:07:53 +0200
Subject: [PATCH 011/209] refactor: return created request file path and update
sanitzation regex
---
.../ReduxStore/slices/collections/actions.js | 14 +++++++++-----
packages/bruno-electron/src/ipc/collection.js | 2 ++
packages/bruno-electron/src/utils/filesystem.js | 2 +-
3 files changed, 12 insertions(+), 6 deletions(-)
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 63cae70c7d..95eea255f5 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
@@ -572,7 +572,7 @@ export const moveItemToRootOfCollection = (collectionUid, draggedItemUid) => (di
export const newHttpRequest = (params) => (dispatch, getState) => {
const { requestName, requestType, requestUrl, requestMethod, collectionUid, itemUid } = params;
- return new Promise((resolve, reject) => {
+ return new Promise(async (resolve, reject) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
if (!collection) {
@@ -620,7 +620,7 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
if (!reqWithSameNameExists) {
const { ipcRenderer } = window;
- ipcRenderer.invoke('renderer:new-request', collection.pathname, item).then(resolve).catch(reject);
+ const newPath = await ipcRenderer.invoke('renderer:new-request', collection.pathname, item);
// the useCollectionNextAction() will track this and open the new request in a new tab
// once the request is created
dispatch(
@@ -628,12 +628,14 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
nextAction: {
type: 'OPEN_REQUEST',
payload: {
- pathname: collection.pathname + PATH_SEPARATOR + sanitizeFilenme(item.name) + '.bru'
+ pathname: newPath
}
},
collectionUid
})
);
+
+ resolve();
} else {
return reject(new Error('Duplicate request names are not allowed under the same folder'));
}
@@ -649,7 +651,7 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
if (!reqWithSameNameExists) {
const { ipcRenderer } = window;
- ipcRenderer.invoke('renderer:new-request', fullName, item).then(resolve).catch(reject);
+ const newPath = await ipcRenderer.invoke('renderer:new-request', fullName, item);
// the useCollectionNextAction() will track this and open the new request in a new tab
// once the request is created
@@ -658,12 +660,14 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
nextAction: {
type: 'OPEN_REQUEST',
payload: {
- pathname: collection.pathname + PATH_SEPARATOR + sanitizeFilenme(item.name) + '.bru'
+ pathname: newPath
}
},
collectionUid
})
);
+
+ resolve();
} else {
return reject(new Error('Duplicate request names are not allowed under the same folder'));
}
diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js
index fe76db6da9..1bb4bf0607 100644
--- a/packages/bruno-electron/src/ipc/collection.js
+++ b/packages/bruno-electron/src/ipc/collection.js
@@ -119,6 +119,8 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
const content = jsonToBru(request);
await writeFile(sanitizedPathname, content);
+
+ return sanitizedPathname;
} catch (error) {
return Promise.reject(error);
}
diff --git a/packages/bruno-electron/src/utils/filesystem.js b/packages/bruno-electron/src/utils/filesystem.js
index 00834cfecd..c523ffea09 100644
--- a/packages/bruno-electron/src/utils/filesystem.js
+++ b/packages/bruno-electron/src/utils/filesystem.js
@@ -119,7 +119,7 @@ const sanitizeDirectoryName = (name) => {
};
const sanitizeFilenme = (name) => {
- return name.replace(/[^\w-_.]/g, '_');
+ return name.replace(/[<>:"/\\|?*\x00-\x1F]/g, '_');
};
module.exports = {
From 804b6ed5a1b41176b50725862ef1bbbf4c5cc3b3 Mon Sep 17 00:00:00 2001
From: Kavinkumar
Date: Mon, 16 Oct 2023 11:10:16 +0530
Subject: [PATCH 012/209] Fix :: error on renaming environment names while
changing cases
---
.../bruno-app/src/utils/system/fileSystem.js | 12 +++++
packages/bruno-electron/src/ipc/collection.js | 54 ++++++++-----------
2 files changed, 34 insertions(+), 32 deletions(-)
create mode 100644 packages/bruno-app/src/utils/system/fileSystem.js
diff --git a/packages/bruno-app/src/utils/system/fileSystem.js b/packages/bruno-app/src/utils/system/fileSystem.js
new file mode 100644
index 0000000000..070b4a298b
--- /dev/null
+++ b/packages/bruno-app/src/utils/system/fileSystem.js
@@ -0,0 +1,12 @@
+const path = require('path');
+
+const fileExistsWithCase = (newFilePath, oldFilePath) => {
+ const newFileName = path.basename(newFilePath);
+ const oldFileName = path.basename(oldFilePath);
+ if (newFileName.toLowerCase() === oldFileName.toLowerCase()) {
+ return false;
+ }
+ return fs.existsSync(newFilePath);
+};
+
+module.exports = { fileExistsWithCase };
diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js
index d69c393c81..d9243ccea0 100644
--- a/packages/bruno-electron/src/ipc/collection.js
+++ b/packages/bruno-electron/src/ipc/collection.js
@@ -20,6 +20,7 @@ const { generateUidBasedOnHash } = require('../utils/common');
const { moveRequestUid, deleteRequestUid } = require('../cache/requestUids');
const { setPreferences } = require('../store/preferences');
const EnvironmentSecretsStore = require('../store/env-secrets');
+const { fileExistsWithCase } = require('@usebruno/app/src/utils/system/fileSystem');
const environmentSecretsStore = new EnvironmentSecretsStore();
@@ -47,7 +48,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
async (event, collectionName, collectionFolderName, collectionLocation) => {
try {
const dirPath = path.join(collectionLocation, collectionFolderName);
- if (fileExists(dirPath)) {
+ if (fs.existsSync(dirPath)) {
throw new Error(`collection: ${dirPath} already exists`);
}
@@ -115,7 +116,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// new request
ipcMain.handle('renderer:new-request', async (event, pathname, request) => {
try {
- if (fileExists(pathname)) {
+ if (fs.existsSync(pathname)) {
throw new Error(`path: ${pathname} already exists`);
}
@@ -129,7 +130,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// save request
ipcMain.handle('renderer:save-request', async (event, pathname, request) => {
try {
- if (!fileExists(pathname)) {
+ if (!fs.existsSync(pathname)) {
throw new Error(`path: ${pathname} does not exist`);
}
@@ -144,12 +145,12 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:create-environment', async (event, collectionPathname, name) => {
try {
const envDirPath = path.join(collectionPathname, 'environments');
- if (!fileExists(envDirPath)) {
+ if (!fs.existsSync(envDirPath)) {
await createDirectory(envDirPath);
}
const envFilePath = path.join(envDirPath, `${name}.bru`);
- if (fileExists(envFilePath)) {
+ if (fs.existsSync(envFilePath)) {
throw new Error(`environment: ${envFilePath} already exists`);
}
@@ -166,12 +167,12 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:copy-environment', async (event, collectionPathname, name, baseVariables) => {
try {
const envDirPath = path.join(collectionPathname, 'environments');
- if (!fileExists(envDirPath)) {
+ if (!fs.existsSync(envDirPath)) {
await createDirectory(envDirPath);
}
const envFilePath = path.join(envDirPath, `${name}.bru`);
- if (fileExists(envFilePath)) {
+ if (fs.existsSync(envFilePath)) {
throw new Error(`environment: ${envFilePath} already exists`);
}
@@ -188,12 +189,12 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:save-environment', async (event, collectionPathname, environment) => {
try {
const envDirPath = path.join(collectionPathname, 'environments');
- if (!fileExists(envDirPath)) {
+ if (!fs.existsSync(envDirPath)) {
await createDirectory(envDirPath);
}
const envFilePath = path.join(envDirPath, `${environment.name}.bru`);
- if (!fileExists(envFilePath)) {
+ if (!fs.existsSync(envFilePath)) {
throw new Error(`environment: ${envFilePath} does not exist`);
}
@@ -213,12 +214,12 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
try {
const envDirPath = path.join(collectionPathname, 'environments');
const envFilePath = path.join(envDirPath, `${environmentName}.bru`);
- if (!fileExists(envFilePath)) {
+ if (!fs.existsSync(envFilePath)) {
throw new Error(`environment: ${envFilePath} does not exist`);
}
const newEnvFilePath = path.join(envDirPath, `${newName}.bru`);
- if (fileExists(newEnvFilePath)) {
+ if (fileExistsWithCase(newEnvFilePath, envFilePath)) {
throw new Error(`environment: ${newEnvFilePath} already exists`);
}
@@ -235,7 +236,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
try {
const envDirPath = path.join(collectionPathname, 'environments');
const envFilePath = path.join(envDirPath, `${environmentName}.bru`);
- if (!fileExists(envFilePath)) {
+ if (!fs.existsSync(envFilePath)) {
throw new Error(`environment: ${envFilePath} does not exist`);
}
@@ -251,10 +252,10 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:rename-item', async (event, oldPath, newPath, newName) => {
try {
// the file existing is checked with case insensitive file name. I want to check with case sensitive
- if (!fileExists(oldPath)) {
+ if (!fs.existsSync(oldPath)) {
throw new Error(`path: ${oldPath} does not exist`);
}
- if (fileExists(newPath)) {
+ if (fileExistsWithCase(newPath, oldPath)) {
throw new Error(`path: ${oldPath} already exists`);
}
@@ -293,7 +294,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// new folder
ipcMain.handle('renderer:new-folder', async (event, pathname) => {
try {
- if (!fileExists(pathname)) {
+ if (!fs.existsSync(pathname)) {
fs.mkdirSync(pathname);
} else {
return Promise.reject(new Error('The directory already exists'));
@@ -307,7 +308,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:delete-item', async (event, pathname, type) => {
try {
if (type === 'folder') {
- if (!fileExists(pathname)) {
+ if (!fs.existsSync(pathname)) {
return Promise.reject(new Error('The directory does not exist'));
}
@@ -319,7 +320,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
fs.rmSync(pathname, { recursive: true, force: true });
} else if (['http-request', 'graphql-request'].includes(type)) {
- if (!fileExists(pathname)) {
+ if (!fs.existsSync(pathname)) {
return Promise.reject(new Error('The file does not exist'));
}
@@ -353,7 +354,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
let collectionName = sanitizeDirectoryName(collection.name);
let collectionPath = path.join(collectionLocation, collectionName);
- if (fileExists(collectionPath)) {
+ if (fs.existsSync(collectionPath)) {
throw new Error(`collection: ${collectionPath} already exists`);
}
@@ -378,7 +379,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
const parseEnvironments = (environments = [], collectionPath) => {
const envDirPath = path.join(collectionPath, 'environments');
- if (!fileExists(envDirPath)) {
+ if (!fs.existsSync(envDirPath)) {
fs.mkdirSync(envDirPath);
}
@@ -449,11 +450,11 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
const folderName = path.basename(folderPath);
const newFolderPath = path.join(destinationPath, folderName);
- if (!fileExists(folderPath)) {
+ if (!fs.existsSync(folderPath)) {
throw new Error(`folder: ${folderPath} does not exist`);
}
- if (fileExists(newFolderPath)) {
+ if (fs.existsSync(newFolderPath)) {
throw new Error(`folder: ${newFolderPath} already exists`);
}
@@ -527,15 +528,4 @@ const registerCollectionsIpc = (mainWindow, watcher, lastOpenedCollections) => {
registerMainEventHandlers(mainWindow, watcher, lastOpenedCollections);
};
-function fileExists(filePath) {
- try {
- const folderPath = path.dirname(filePath);
- const fileName = path.basename(filePath);
- const files = fs.readdirSync(folderPath);
- return files.includes(fileName);
- } catch (error) {
- return false;
- }
-}
-
module.exports = registerCollectionsIpc;
From 66e4ffc38347faf1dcaf8ab5bdbe63f2cdef5592 Mon Sep 17 00:00:00 2001
From: Kavinkumar
Date: Tue, 17 Oct 2023 10:50:19 +0530
Subject: [PATCH 013/209] Fix :: error on renaming environment names while
changing cases
---
packages/bruno-app/src/utils/system/fileSystem.js | 12 ------------
packages/bruno-electron/src/ipc/collection.js | 4 ++--
packages/bruno-electron/src/utils/filesystem.js | 12 +++++++++++-
3 files changed, 13 insertions(+), 15 deletions(-)
delete mode 100644 packages/bruno-app/src/utils/system/fileSystem.js
diff --git a/packages/bruno-app/src/utils/system/fileSystem.js b/packages/bruno-app/src/utils/system/fileSystem.js
deleted file mode 100644
index 070b4a298b..0000000000
--- a/packages/bruno-app/src/utils/system/fileSystem.js
+++ /dev/null
@@ -1,12 +0,0 @@
-const path = require('path');
-
-const fileExistsWithCase = (newFilePath, oldFilePath) => {
- const newFileName = path.basename(newFilePath);
- const oldFileName = path.basename(oldFilePath);
- if (newFileName.toLowerCase() === oldFileName.toLowerCase()) {
- return false;
- }
- return fs.existsSync(newFilePath);
-};
-
-module.exports = { fileExistsWithCase };
diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js
index d9243ccea0..5a2c257580 100644
--- a/packages/bruno-electron/src/ipc/collection.js
+++ b/packages/bruno-electron/src/ipc/collection.js
@@ -12,7 +12,8 @@ const {
browseDirectory,
createDirectory,
searchForBruFiles,
- sanitizeDirectoryName
+ sanitizeDirectoryName,
+ fileExistsWithCase
} = require('../utils/filesystem');
const { stringifyJson } = require('../utils/common');
const { openCollectionDialog, openCollection } = require('../app/collections');
@@ -20,7 +21,6 @@ const { generateUidBasedOnHash } = require('../utils/common');
const { moveRequestUid, deleteRequestUid } = require('../cache/requestUids');
const { setPreferences } = require('../store/preferences');
const EnvironmentSecretsStore = require('../store/env-secrets');
-const { fileExistsWithCase } = require('@usebruno/app/src/utils/system/fileSystem');
const environmentSecretsStore = new EnvironmentSecretsStore();
diff --git a/packages/bruno-electron/src/utils/filesystem.js b/packages/bruno-electron/src/utils/filesystem.js
index b55dfd7258..517c2d21b8 100644
--- a/packages/bruno-electron/src/utils/filesystem.js
+++ b/packages/bruno-electron/src/utils/filesystem.js
@@ -118,6 +118,15 @@ const sanitizeDirectoryName = (name) => {
return name.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '-');
};
+const fileExistsWithCase = (newFilePath, oldFilePath) => {
+ const newFileName = path.basename(newFilePath);
+ const oldFileName = path.basename(oldFilePath);
+ if (newFileName.toLowerCase() === oldFileName.toLowerCase()) {
+ return false;
+ }
+ return fs.existsSync(newFilePath);
+};
+
module.exports = {
isValidPathname,
exists,
@@ -132,5 +141,6 @@ module.exports = {
browseDirectory,
searchForFiles,
searchForBruFiles,
- sanitizeDirectoryName
+ sanitizeDirectoryName,
+ fileExistsWithCase
};
From 8e68b8ff22e422a615fe113e5a6b07750044fa10 Mon Sep 17 00:00:00 2001
From: Its-treason <39559178+Its-treason@users.noreply.github.com>
Date: Thu, 19 Oct 2023 17:00:42 +0200
Subject: [PATCH 014/209] chore: Fix typos
---
.../CreateEnvironment/index.js | 2 +-
.../Sidebar/CreateCollection/index.js | 2 +-
.../src/components/Sidebar/NewFolder/index.js | 2 +-
.../ReduxStore/slices/collections/actions.js | 1 +
packages/bruno-electron/src/ipc/collection.js | 36 +++++++++----------
.../bruno-electron/src/utils/filesystem.js | 4 +--
6 files changed, 24 insertions(+), 23 deletions(-)
diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/CreateEnvironment/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/CreateEnvironment/index.js
index d86a0641e0..a2fe4ffbee 100644
--- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/CreateEnvironment/index.js
+++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/CreateEnvironment/index.js
@@ -17,7 +17,7 @@ const CreateEnvironment = ({ collection, onClose }) => {
},
validationSchema: Yup.object({
name: Yup.string()
- .min(1, 'must be atleast 1 characters')
+ .min(1, 'must be at least 1 character')
.max(250, 'must be 250 characters or less')
.required('name is required')
}),
diff --git a/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js b/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js
index 89620eb236..d4ac77a4ba 100644
--- a/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js
+++ b/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js
@@ -26,9 +26,9 @@ const CreateCollection = ({ onClose }) => {
.max(50, 'must be 50 characters or less')
.required('collection name is required'),
collectionFolderName: Yup.string()
- .max(250, 'must be 250 characters or less')
.trim()
.matches(dirnameRegex, 'Folder name contains invalid characters')
+ .max(250, 'must be 250 characters or less')
.min(1, 'must be at least 1 character')
.required('folder name is required'),
collectionLocation: Yup.string().min(1, 'location is required').required('location is required')
diff --git a/packages/bruno-app/src/components/Sidebar/NewFolder/index.js b/packages/bruno-app/src/components/Sidebar/NewFolder/index.js
index cad779b63c..e1a8464aee 100644
--- a/packages/bruno-app/src/components/Sidebar/NewFolder/index.js
+++ b/packages/bruno-app/src/components/Sidebar/NewFolder/index.js
@@ -21,12 +21,12 @@ const NewFolder = ({ collection, item, onClose }) => {
.min(1, 'must be at least 1 character')
.required('name is required')
.max(250, 'must be 250 characters or less')
- .trim()
.matches(dirnameRegex, 'Folder name contains invalid characters')
.test({
name: 'folderName',
message: 'The folder name "environments" at the root of the collection is reserved in bruno',
test: (value) => {
+ // If the the item has a uid, it is inside a sub folder
if (item && item.uid) {
return true;
}
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 52203a0e9e..0225b8a377 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
@@ -280,6 +280,7 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta
if (!collection) {
return reject(new Error('Collection not found'));
}
+
const collectionCopy = cloneDeep(collection);
const item = findItemInCollection(collectionCopy, itemUid);
if (!item) {
diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js
index a104e34c13..d670af7c23 100644
--- a/packages/bruno-electron/src/ipc/collection.js
+++ b/packages/bruno-electron/src/ipc/collection.js
@@ -12,7 +12,7 @@ const {
createDirectory,
searchForBruFiles,
sanitizeDirectoryName,
- sanitizeFilenme
+ sanitizeFilename
} = require('../utils/filesystem');
const { stringifyJson } = require('../utils/common');
const { openCollectionDialog } = require('../app/collections');
@@ -104,7 +104,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// new request
ipcMain.handle('renderer:new-request', async (event, pathname, request) => {
try {
- const sanitizedPathname = path.join(pathname, sanitizeFilenme(request.name) + '.bru');
+ const sanitizedPathname = path.join(pathname, sanitizeFilename(request.name) + '.bru');
if (fs.existsSync(sanitizedPathname)) {
throw new Error(`path: ${sanitizedPathname} already exists`);
@@ -141,8 +141,8 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
await createDirectory(envDirPath);
}
- const filenameSanatized = sanitizeFilenme(`${name}.bru`);
- const envFilePath = path.join(envDirPath, filenameSanatized);
+ const filenameSanitized = sanitizeFilename(`${name}.bru`);
+ const envFilePath = path.join(envDirPath, filenameSanitized);
if (fs.existsSync(envFilePath)) {
throw new Error(`environment: ${envFilePath} already exists`);
}
@@ -172,9 +172,9 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
await createDirectory(envDirPath);
}
- let envFilePath = path.join(envDirPath, `${sanitizeFilenme(environment.name)}.bru`);
+ let envFilePath = path.join(envDirPath, `${sanitizeFilename(environment.name)}.bru`);
if (!fs.existsSync(envFilePath)) {
- // Fallback to unsatized filename for old envs
+ // Fallback to unsanitized filename for old envs
envFilePath = path.join(envDirPath, `${environment.name}.bru`);
if (!fs.existsSync(envFilePath)) {
throw new Error(`environment: ${envFilePath} does not exist`);
@@ -196,16 +196,16 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:rename-environment', async (event, collectionPathname, environmentName, newName) => {
try {
const envDirPath = path.join(collectionPathname, 'environments');
- let envFilePath = path.join(envDirPath, `${sanitizeFilenme(environmentName)}.bru`);
+ let envFilePath = path.join(envDirPath, `${sanitizeFilename(environmentName)}.bru`);
if (!fs.existsSync(envFilePath)) {
- // Fallback to unsatized env name
+ // Fallback to unsanitized env name
envFilePath = path.join(envDirPath, `${environmentName}.bru`);
if (!fs.existsSync(envFilePath)) {
throw new Error(`environment: ${envFilePath} does not exist`);
}
}
- const newEnvFilePath = path.join(envDirPath, `${sanitizeFilenme(newName)}.bru`);
+ const newEnvFilePath = path.join(envDirPath, `${sanitizeFilename(newName)}.bru`);
if (fs.existsSync(newEnvFilePath) && envFilePath !== newEnvFilePath) {
throw new Error(`environment: ${newEnvFilePath} already exists`);
}
@@ -228,9 +228,9 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:delete-environment', async (event, collectionPathname, environmentName) => {
try {
const envDirPath = path.join(collectionPathname, 'environments');
- let envFilePath = path.join(envDirPath, `${sanitizeFilenme(environmentName)}.bru`);
+ let envFilePath = path.join(envDirPath, `${sanitizeFilename(environmentName)}.bru`);
if (!fs.existsSync(envFilePath)) {
- // Fallback to unsatized env name
+ // Fallback to unsanitized env name
envFilePath = path.join(envDirPath, `${environmentName}.bru`);
if (!fs.existsSync(envFilePath)) {
throw new Error(`environment: ${envFilePath} does not exist`);
@@ -273,9 +273,9 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
throw new Error(`path: ${oldPathFull} is not a bru file`);
}
- const newSantitizedPath = path.join(newPath, sanitizeFilenme(newName) + '.bru');
- if (fs.existsSync(newSantitizedPath) && newSantitizedPath !== oldPathFull) {
- throw new Error(`path: ${newSantitizedPath} already exists`);
+ const newSanitizedPath = path.join(newPath, sanitizeFilename(newName) + '.bru');
+ if (fs.existsSync(newSanitizedPath) && newSanitizedPath !== oldPathFull) {
+ throw new Error(`path: ${newSanitizedPath} already exists`);
}
// update name in file and save new copy, then delete old copy
@@ -284,13 +284,13 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
jsonData.name = newName;
- moveRequestUid(oldPathFull, newSantitizedPath);
+ moveRequestUid(oldPathFull, newSanitizedPath);
const content = jsonToBru(jsonData);
- await writeFile(newSantitizedPath, content);
+ await writeFile(newSanitizedPath, content);
- // Because of santization the name can change but the path stays the same
- if (newSantitizedPath !== oldPathFull) {
+ // Because of sanitization the name can change but the path stays the same
+ if (newSanitizedPath !== oldPathFull) {
fs.unlinkSync(oldPathFull);
}
} catch (error) {
diff --git a/packages/bruno-electron/src/utils/filesystem.js b/packages/bruno-electron/src/utils/filesystem.js
index c523ffea09..6c2bba5cc6 100644
--- a/packages/bruno-electron/src/utils/filesystem.js
+++ b/packages/bruno-electron/src/utils/filesystem.js
@@ -118,7 +118,7 @@ const sanitizeDirectoryName = (name) => {
return name.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '-');
};
-const sanitizeFilenme = (name) => {
+const sanitizeFilename = (name) => {
return name.replace(/[<>:"/\\|?*\x00-\x1F]/g, '_');
};
@@ -137,5 +137,5 @@ module.exports = {
searchForFiles,
searchForBruFiles,
sanitizeDirectoryName,
- sanitizeFilenme
+ sanitizeFilename
};
From 8ed385fe9d578a35e3976509f6af8737e2ab0b56 Mon Sep 17 00:00:00 2001
From: Jonathan Gruber
Date: Thu, 19 Oct 2023 10:28:37 +0200
Subject: [PATCH 015/209] chore(#673): remove obsolete mustache.js library
---
package-lock.json | 14 --------------
packages/bruno-cli/package.json | 1 -
packages/bruno-cli/src/utils/bru.js | 8 +-------
packages/bruno-electron/package.json | 1 -
packages/bruno-electron/src/ipc/network/index.js | 8 +-------
5 files changed, 2 insertions(+), 30 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 79f3de50f3..1fe51774c1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11599,13 +11599,6 @@
"node": ">=8"
}
},
- "node_modules/mustache": {
- "version": "4.2.0",
- "license": "MIT",
- "bin": {
- "mustache": "bin/mustache"
- }
- },
"node_modules/mute-stream": {
"version": "0.0.8",
"license": "ISC"
@@ -16554,7 +16547,6 @@
"https-proxy-agent": "^7.0.2",
"inquirer": "^9.1.4",
"lodash": "^4.17.21",
- "mustache": "^4.2.0",
"qs": "^6.11.0",
"socks-proxy-agent": "^8.0.2",
"yargs": "^17.6.2"
@@ -16658,7 +16650,6 @@
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"mime-types": "^2.1.35",
- "mustache": "^4.2.0",
"nanoid": "3.3.4",
"node-machine-id": "^1.1.12",
"qs": "^6.11.0",
@@ -20605,7 +20596,6 @@
"https-proxy-agent": "^7.0.2",
"inquirer": "^9.1.4",
"lodash": "^4.17.21",
- "mustache": "^4.2.0",
"qs": "^6.11.0",
"socks-proxy-agent": "^8.0.2",
"yargs": "^17.6.2"
@@ -21542,7 +21532,6 @@
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"mime-types": "^2.1.35",
- "mustache": "^4.2.0",
"nanoid": "3.3.4",
"node-machine-id": "^1.1.12",
"qs": "^6.11.0",
@@ -25260,9 +25249,6 @@
}
}
},
- "mustache": {
- "version": "4.2.0"
- },
"mute-stream": {
"version": "0.0.8"
},
diff --git a/packages/bruno-cli/package.json b/packages/bruno-cli/package.json
index 54fb85fbfd..dbd6c6f319 100644
--- a/packages/bruno-cli/package.json
+++ b/packages/bruno-cli/package.json
@@ -37,7 +37,6 @@
"https-proxy-agent": "^7.0.2",
"inquirer": "^9.1.4",
"lodash": "^4.17.21",
- "mustache": "^4.2.0",
"qs": "^6.11.0",
"socks-proxy-agent": "^8.0.2",
"yargs": "^17.6.2"
diff --git a/packages/bruno-cli/src/utils/bru.js b/packages/bruno-cli/src/utils/bru.js
index 34fb09c6b0..80a4813f16 100644
--- a/packages/bruno-cli/src/utils/bru.js
+++ b/packages/bruno-cli/src/utils/bru.js
@@ -1,12 +1,6 @@
const _ = require('lodash');
-const Mustache = require('mustache');
const { bruToEnvJsonV2, bruToJsonV2, collectionBruToJson: _collectionBruToJson } = require('@usebruno/lang');
-// override the default escape function to prevent escaping
-Mustache.escape = function (value) {
- return value;
-};
-
const collectionBruToJson = (bru) => {
try {
const json = _collectionBruToJson(bru);
@@ -96,7 +90,7 @@ const getEnvVars = (environment = {}) => {
const envVars = {};
_.each(variables, (variable) => {
if (variable.enabled) {
- envVars[variable.name] = Mustache.escape(variable.value);
+ envVars[variable.name] = variable.value;
}
});
diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json
index 47753da5a2..12d7cfe600 100644
--- a/packages/bruno-electron/package.json
+++ b/packages/bruno-electron/package.json
@@ -45,7 +45,6 @@
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"mime-types": "^2.1.35",
- "mustache": "^4.2.0",
"nanoid": "3.3.4",
"node-machine-id": "^1.1.12",
"qs": "^6.11.0",
diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js
index 7c37256e93..0d732c1140 100644
--- a/packages/bruno-electron/src/ipc/network/index.js
+++ b/packages/bruno-electron/src/ipc/network/index.js
@@ -5,7 +5,6 @@ const https = require('https');
const axios = require('axios');
const path = require('path');
const decomment = require('decomment');
-const Mustache = require('mustache');
const FormData = require('form-data');
const contentDispositionParser = require('content-disposition');
const mime = require('mime-types');
@@ -29,11 +28,6 @@ const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-he
const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../../utils/proxy-util');
const { chooseFileToSave, writeBinaryFile } = require('../../utils/filesystem');
-// override the default escape function to prevent escaping
-Mustache.escape = function (value) {
- return value;
-};
-
const safeStringifyJSON = (data) => {
try {
return JSON.stringify(data);
@@ -61,7 +55,7 @@ const getEnvVars = (environment = {}) => {
const envVars = {};
each(variables, (variable) => {
if (variable.enabled) {
- envVars[variable.name] = Mustache.escape(variable.value);
+ envVars[variable.name] = variable.value;
}
});
From 8848da16e7dc526c43d04e633d373f6b3d84c8a9 Mon Sep 17 00:00:00 2001
From: Its-treason <39559178+Its-treason@users.noreply.github.com>
Date: Wed, 13 Dec 2023 22:33:52 +0100
Subject: [PATCH 016/209] fix(#352): Variable interpolation for multipart form
body
- Moved creation of FormData object into interpolation phase
- BREAKING: This means that one can no longer access FormData directly
in the pre-request script, instead the body is now an object.
---
package-lock.json | 2 +-
packages/bruno-cli/changelog.md | 4 ++++
.../bruno-cli/src/runner/interpolate-vars.js | 13 ++++++++++++-
.../bruno-cli/src/runner/run-single-request.js | 14 +-------------
.../src/ipc/network/interpolate-vars.js | 13 ++++++++++++-
.../src/ipc/network/prepare-request.js | 16 ++++------------
6 files changed, 34 insertions(+), 28 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 1c24873541..06272dfdbb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17670,7 +17670,7 @@
},
"packages/bruno-electron": {
"name": "bruno",
- "version": "v1.3.0",
+ "version": "v1.4.0",
"dependencies": {
"@aws-sdk/credential-providers": "^3.425.0",
"@usebruno/js": "0.9.3",
diff --git a/packages/bruno-cli/changelog.md b/packages/bruno-cli/changelog.md
index 1d6729309b..757de80376 100644
--- a/packages/bruno-cli/changelog.md
+++ b/packages/bruno-cli/changelog.md
@@ -1,5 +1,9 @@
# Changelog
+## Unreleased
+
+- Fixed variable interpolation for multipart form body
+
## 1.1.0
- Upgraded axios to 1.5.1
diff --git a/packages/bruno-cli/src/runner/interpolate-vars.js b/packages/bruno-cli/src/runner/interpolate-vars.js
index b926164785..4bdd4fa68c 100644
--- a/packages/bruno-cli/src/runner/interpolate-vars.js
+++ b/packages/bruno-cli/src/runner/interpolate-vars.js
@@ -1,5 +1,6 @@
const Handlebars = require('handlebars');
-const { each, forOwn, cloneDeep } = require('lodash');
+const FormData = require('form-data');
+const { each, forOwn, cloneDeep, extend } = require('lodash');
const getContentType = (headers = {}) => {
let contentType = '';
@@ -90,6 +91,16 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
request.data = JSON.parse(parsed);
} catch (err) {}
}
+ } else if (contentType === 'multipart/form-data') {
+ // make axios work in node using form data
+ // reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
+ const form = new FormData();
+ forOwn(request.data, (value, name) => {
+ form.append(interpolate(name), interpolate(value));
+ });
+
+ extend(request.headers, form.getHeaders());
+ request.data = form;
} else {
request.data = interpolate(request.data);
}
diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js
index b19289eed0..53de5dddfa 100644
--- a/packages/bruno-cli/src/runner/run-single-request.js
+++ b/packages/bruno-cli/src/runner/run-single-request.js
@@ -3,8 +3,7 @@ const qs = require('qs');
const chalk = require('chalk');
const decomment = require('decomment');
const fs = require('fs');
-const { forOwn, isUndefined, isNull, each, extend, get, compact } = require('lodash');
-const FormData = require('form-data');
+const { isUndefined, isNull, each, get, compact } = require('lodash');
const prepareRequest = require('./prepare-request');
const interpolateVars = require('./interpolate-vars');
const { interpolateString } = require('./interpolate-string');
@@ -37,17 +36,6 @@ const runSingleRequest = async function (
const scriptingConfig = get(brunoConfig, 'scripts', {});
- // make axios work in node using form data
- // reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
- if (request.headers && request.headers['content-type'] === 'multipart/form-data') {
- const form = new FormData();
- forOwn(request.data, (value, key) => {
- form.append(key, value);
- });
- extend(request.headers, form.getHeaders());
- request.data = form;
- }
-
// run pre-request vars
const preRequestVars = get(bruJson, 'request.vars.req');
if (preRequestVars?.length) {
diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js
index 4a709f5aed..5bea7de2c1 100644
--- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js
+++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js
@@ -1,5 +1,6 @@
const Handlebars = require('handlebars');
-const { each, forOwn, cloneDeep } = require('lodash');
+const FormData = require('form-data');
+const { each, forOwn, cloneDeep, extend } = require('lodash');
const getContentType = (headers = {}) => {
let contentType = '';
@@ -92,6 +93,16 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
request.data = JSON.parse(parsed);
} catch (err) {}
}
+ } else if (request.mode === 'multipartForm') {
+ // make axios work in node using form data
+ // reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
+ const form = new FormData();
+ forOwn(request.data, (value, name) => {
+ form.append(interpolate(name), interpolate(value));
+ });
+
+ extend(request.headers, form.getHeaders());
+ request.data = form;
} else {
request.data = interpolate(request.data);
}
diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js
index 761984e658..eb96229f45 100644
--- a/packages/bruno-electron/src/ipc/network/prepare-request.js
+++ b/packages/bruno-electron/src/ipc/network/prepare-request.js
@@ -1,4 +1,4 @@
-const { get, each, filter, forOwn, extend } = require('lodash');
+const { get, each, filter } = require('lodash');
const decomment = require('decomment');
const FormData = require('form-data');
@@ -146,20 +146,12 @@ const prepareRequest = (request, collectionRoot) => {
}
if (request.body.mode === 'multipartForm') {
- const params = {};
const enabledParams = filter(request.body.multipartForm, (p) => p.enabled);
+
+ const params = {};
each(enabledParams, (p) => (params[p.name] = p.value));
- axiosRequest.headers['content-type'] = 'multipart/form-data';
axiosRequest.data = params;
-
- // make axios work in node using form data
- // reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
- const form = new FormData();
- forOwn(axiosRequest.data, (value, key) => {
- form.append(key, value);
- });
- extend(axiosRequest.headers, form.getHeaders());
- axiosRequest.data = form;
+ axiosRequest.headers['content-type'] = 'multipart/form-data';
}
if (request.body.mode === 'graphql') {
From 136993ebec498a407c47099ea20a83c3bdc9f5f5 Mon Sep 17 00:00:00 2001
From: Baptiste POULAIN
Date: Thu, 14 Dec 2023 17:22:32 +0100
Subject: [PATCH 017/209] feature(react-codemirror): add basic CodeEditor2,
need to implement shortcuts and add style
---
bun.lockb | Bin 0 -> 290418 bytes
packages/bruno-app/package.json | 4 +++-
.../src/components/CodeEditor/index.js | 1 -
.../src/components/CodeEditor2/index.jsx | 14 ++++++++++++++
.../QueryResult/QueryResultPreview/index.js | 3 ++-
5 files changed, 19 insertions(+), 3 deletions(-)
create mode 100755 bun.lockb
create mode 100644 packages/bruno-app/src/components/CodeEditor2/index.jsx
diff --git a/bun.lockb b/bun.lockb
new file mode 100755
index 0000000000000000000000000000000000000000..d96cfe57454b36265e49ad6f7444779598cd679e
GIT binary patch
literal 290418
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