{
+ this._node = node;
+ }}
+ style={{ height: '100%' }}
+ />
+
+ );
+ }
+
+ addOverlay = () => {
+ const mode = this.props.mode || 'application/ld+json';
+ let variables = getEnvironmentVariables(this.props.collection);
+ this.variables = variables;
+
+ defineCodeMirrorBrunoVariablesMode(variables, mode);
+ this.editor.setOption('mode', 'brunovariables');
+ };
+
+ _onEdit = () => {
+ if (!this.ignoreChangeEvent && this.editor) {
+ this.editor.setOption('lint', this.editor.getValue().trim().length > 0 ? this.lintOptions : false);
+ this.cachedValue = this.editor.getValue();
+ if (this.props.onEdit) {
+ this.props.onEdit(this.cachedValue);
+ }
+ }
+ };
+
+ _onScroll = () => {
+ if (!this.editor) return;
+ const wrapper = this.editor.getWrapperElement();
+ if (wrapper && wrapper.offsetParent === null) return;
+ this._lastScrollTop = this.editor.getScrollInfo().top;
+ if (typeof this.props.onScroll === 'function') {
+ this.props.onScroll(this._lastScrollTop);
+ }
+ };
+}
diff --git a/packages/bruno-app/src/components/FileEditor/index.js b/packages/bruno-app/src/components/FileEditor/index.js
new file mode 100644
index 000000000..7544a1a08
--- /dev/null
+++ b/packages/bruno-app/src/components/FileEditor/index.js
@@ -0,0 +1,68 @@
+import get from 'lodash/get';
+import { useTheme } from 'providers/Theme';
+import { useDispatch, useSelector } from 'react-redux';
+import CodeEditor from './CodeEditor/index';
+import { saveFile } from 'providers/ReduxStore/slices/collections/actions';
+import { IconDeviceFloppy } from '@tabler/icons';
+import { toggleCollectionFileMode, updateFileContent } from 'providers/ReduxStore/slices/collections';
+import { usePersistedState } from 'hooks/usePersistedState';
+
+const FileEditor = ({ item, collection }) => {
+ const dispatch = useDispatch();
+ const { displayedTheme, theme } = useTheme();
+ const preferences = useSelector((state) => state.app.preferences);
+ const [scroll, setScroll] = usePersistedState({ key: `file-mode-scroll-${item.uid}`, default: 0 });
+
+ const content = item.draft ? item.draft.raw : item.raw || '';
+
+ const onEdit = (value) => {
+ dispatch(
+ updateFileContent({
+ content: value,
+ itemUid: item.uid,
+ collectionUid: collection.uid
+ })
+ );
+ };
+
+ const hasChanges = item.draft != null;
+
+ const onSave = () => {
+ if (!hasChanges) return;
+ dispatch(saveFile(content, item?.uid, collection?.uid));
+ };
+
+ const _toggleFileMode = () => {
+ dispatch(toggleCollectionFileMode({ collectionUid: collection.uid }));
+ };
+
+ const editorMode = item?.type == 'js' ? 'javascript' : item?.type == 'json' ? 'javascript' : 'application/text';
+
+ return (
+
+
+
+
+ );
+};
+
+export default FileEditor;
diff --git a/packages/bruno-app/src/components/RequestTabPanel/index.js b/packages/bruno-app/src/components/RequestTabPanel/index.js
index c5d2ccfcd..81683b86e 100644
--- a/packages/bruno-app/src/components/RequestTabPanel/index.js
+++ b/packages/bruno-app/src/components/RequestTabPanel/index.js
@@ -19,6 +19,7 @@ import VariablesEditor from 'components/VariablesEditor';
import CollectionSettings from 'components/CollectionSettings';
import { DocExplorer } from '@usebruno/graphql-docs';
+import FileEditor from 'components/FileEditor';
import StyledWrapper from './StyledWrapper';
import FolderSettings from 'components/FolderSettings';
import { getGlobalEnvironmentVariables, getGlobalEnvironmentVariablesMasked } from 'utils/collections/index';
@@ -476,6 +477,17 @@ const RequestTabPanel = () => {
}));
}
};
+
+ if (collection.fileMode) {
+ return (
+
+
+
+
+
+ );
+ }
+
const renderQueryUrl = () => {
if (isGrpcRequest) {
return
;
diff --git a/packages/bruno-app/src/components/RequestTabs/CollectionHeader/index.js b/packages/bruno-app/src/components/RequestTabs/CollectionHeader/index.js
index 1398862a9..ff14ba497 100644
--- a/packages/bruno-app/src/components/RequestTabs/CollectionHeader/index.js
+++ b/packages/bruno-app/src/components/RequestTabs/CollectionHeader/index.js
@@ -12,12 +12,15 @@ import {
IconX,
IconCheck,
IconFolder,
- IconUpload
+ IconUpload,
+ IconFileCode,
+ IconFileOff
} from '@tabler/icons';
import OpenAPISyncIcon from 'components/Icons/OpenAPISync';
import { switchWorkspace, renameWorkspaceAction, exportWorkspaceAction, confirmWorkspaceCreation, cancelWorkspaceCreation } from 'providers/ReduxStore/slices/workspaces/actions';
import { updateWorkspace } from 'providers/ReduxStore/slices/workspaces';
import { showInFolder } from 'providers/ReduxStore/slices/collections/actions';
+import { toggleCollectionFileMode } from 'providers/ReduxStore/slices/collections';
import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
import { uuid } from 'utils/common';
import toast from 'react-hot-toast';
@@ -220,9 +223,18 @@ const CollectionHeader = ({ collection, isScratchCollection }) => {
}));
};
+ const handleFileModeClick = () => {
+ dispatch(
+ toggleCollectionFileMode({
+ collectionUid: collection.uid
+ })
+ );
+ };
+
// Build overflow menu items for the "..." dropdown
const overflowMenuItems = [
{ id: 'variables', label: 'Variables', leftSection: IconEye, onClick: viewVariables },
+ { id: 'file-mode', label: collection.fileMode ? 'Switch to Code Mode' : 'Switch to File Mode', leftSection: collection.fileMode ? IconFileOff : IconFileCode, onClick: handleFileModeClick },
...(isOpenAPISyncEnabled && !hasOpenApiSyncConfigured
? [{ id: 'openapi-sync', label: 'OpenAPI', leftSection: OpenAPISyncIcon, rightSection:
Beta, onClick: viewOpenApiSync }]
: []),
diff --git a/packages/bruno-app/src/providers/ReduxStore/middlewares/autosave/middleware.js b/packages/bruno-app/src/providers/ReduxStore/middlewares/autosave/middleware.js
index 3810e3673..e12c0b36b 100644
--- a/packages/bruno-app/src/providers/ReduxStore/middlewares/autosave/middleware.js
+++ b/packages/bruno-app/src/providers/ReduxStore/middlewares/autosave/middleware.js
@@ -1,4 +1,4 @@
-import { saveRequest, saveCollectionSettings, saveFolderRoot, saveEnvironment } from '../../slices/collections/actions';
+import { saveRequest, saveCollectionSettings, saveFolderRoot, saveFile, saveEnvironment } from '../../slices/collections/actions';
import { saveGlobalEnvironment } from '../../slices/global-environments';
import { flattenItems, isItemARequest, isItemAFolder, findItemInCollection, findCollectionByUid, isItemTransientRequest } from 'utils/collections';
@@ -52,6 +52,7 @@ const actionsToIntercept = [
'collections/updateItemSettings',
'collections/addRequestTag',
'collections/deleteRequestTag',
+ 'collections/updateFileContent',
// Folder-level actions
'collections/addFolderHeader',
@@ -129,11 +130,19 @@ const saveExistingDrafts = (dispatch, getState, interval) => {
}
}
- // Check all items (requests and folders) for drafts
+ // Check all items (requests, folders, and file mode) for drafts
const allItems = flattenItems(collection.items);
allItems.forEach((item) => {
if (item.draft) {
- if (isItemARequest(item)) {
+ // File mode (requests with raw draft content, including empty content)
+ if (collection.fileMode && typeof item.draft.raw === 'string') {
+ // Skip auto-save for transient requests
+ if (isItemTransientRequest(item)) {
+ return;
+ }
+ const key = `file-${item.uid}`;
+ scheduleAutoSave(key, () => dispatch(saveFile(item.draft.raw, item.uid, collection.uid, true)), interval);
+ } else if (isItemARequest(item)) {
// Skip auto-save for transient requests
if (isItemTransientRequest(item)) {
return;
@@ -213,6 +222,13 @@ const determineSaveHandler = (actionType, payload, dispatch, getState) => {
}
}
+ if (actionType === 'collections/updateFileContent') {
+ return {
+ key: `file-${itemUid}`,
+ save: () => dispatch(saveFile(payload.content, itemUid, collectionUid, true))
+ };
+ }
+
return {
key: `request-${itemUid}`,
save: () => dispatch(saveRequest(itemUid, collectionUid, 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 93dc70178..9e605fe24 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
@@ -11,6 +11,7 @@ import path, { normalizePath, isPathExternalToBasePath } from 'utils/common/path
import { insertTaskIntoQueue, toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';
import toast from 'react-hot-toast';
import IpcErrorModal from 'components/Errors/IpcErrorModal/index';
+import SaveFileErrorModal from 'components/Errors/SaveFileErrorModal/index';
import {
findCollectionByUid,
findEnvironmentInCollection,
@@ -193,6 +194,59 @@ export const saveRequest = (itemUid, collectionUid, silent = false) => (dispatch
});
};
+export const saveFile = (content, itemUid, collectionUid, silent = false) => async (dispatch, getState) => {
+ const state = getState();
+ const collection = findCollectionByUid(state.collections.collections, collectionUid);
+ const tempDirectory = state.collections.tempDirectories?.[collectionUid];
+
+ if (!collection) {
+ throw new Error('Collection not found');
+ }
+
+ const collectionCopy = cloneDeep(collection);
+ const item = findItemInCollection(collectionCopy, itemUid);
+
+ // Item is not used to save the bru file
+ // This is to validate if the bru content is associated with a valid item
+ if (!item) {
+ throw new Error('Not able to locate item');
+ }
+
+ const isTransient = tempDirectory && item.pathname.startsWith(tempDirectory);
+ if (isTransient) {
+ if (!silent) {
+ dispatch(addSaveTransientRequestModal({ item, collection }));
+ }
+ throw new Error('Cannot save transient request');
+ }
+
+ const { ipcRenderer } = window;
+ try {
+ if (['http-request', 'graphql-request'].includes(item?.type)) {
+ let json = await ipcRenderer.invoke('renderer:convert-to-json', item, content, collection.format);
+ delete json.isTransient;
+ await itemSchema.validate(json);
+ }
+ } catch (err) {
+ if (!silent) {
+ toast.custom(
);
+ }
+ throw err;
+ }
+
+ try {
+ await ipcRenderer.invoke('renderer:save-file', item.pathname, content);
+ if (!silent) {
+ toast.success('File saved successfully!');
+ }
+ } catch (err) {
+ if (!silent) {
+ toast.error('Failed to save file!');
+ }
+ throw err;
+ }
+};
+
export const saveMultipleRequests = (items) => (dispatch, getState) => {
const state = getState();
const { collections } = state.collections;
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/file-mode.spec.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/file-mode.spec.js
new file mode 100644
index 000000000..63dfd6eb6
--- /dev/null
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/file-mode.spec.js
@@ -0,0 +1,261 @@
+import reducer, {
+ createCollection,
+ toggleCollectionFileMode,
+ updateFileContent,
+ collectionChangeFileEvent
+} from 'providers/ReduxStore/slices/collections';
+
+const COLLECTION_UID = 'col-1';
+const ITEM_UID = 'req-1';
+
+const makeRequest = (overrides = {}) => ({
+ url: 'https://example.com/userinfo',
+ method: 'GET',
+ params: [],
+ headers: [],
+ auth: { mode: 'none' },
+ body: { mode: 'none' },
+ script: {},
+ vars: {},
+ assertions: [],
+ tests: '',
+ docs: '',
+ ...overrides
+});
+
+const makeInitialState = ({ fileMode = false, item = {} } = {}) => ({
+ collections: [
+ {
+ uid: COLLECTION_UID,
+ pathname: '/coll',
+ fileMode,
+ items: [
+ {
+ uid: ITEM_UID,
+ name: 'user_info',
+ filename: 'user_info.bru',
+ pathname: '/coll/user_info.bru',
+ type: 'http-request',
+ seq: 1,
+ raw: 'meta {\n name: user_info\n}',
+ draft: null,
+ request: makeRequest(),
+ ...item
+ }
+ ]
+ }
+ ],
+ collectionSortOrder: 'default',
+ activeWorkspaceUid: null
+});
+
+describe('createCollection', () => {
+ test('initializes fileMode to false', () => {
+ const state = reducer(
+ { collections: [] },
+ createCollection({ uid: COLLECTION_UID, pathname: '/coll', items: [], brunoConfig: {} })
+ );
+
+ expect(state.collections).toHaveLength(1);
+ expect(state.collections[0].fileMode).toBe(false);
+ });
+});
+
+describe('toggleCollectionFileMode', () => {
+ test('toggles fileMode on and off', () => {
+ let state = makeInitialState();
+
+ state = reducer(state, toggleCollectionFileMode({ collectionUid: COLLECTION_UID }));
+ expect(state.collections[0].fileMode).toBe(true);
+
+ state = reducer(state, toggleCollectionFileMode({ collectionUid: COLLECTION_UID }));
+ expect(state.collections[0].fileMode).toBe(false);
+ });
+
+ test('does nothing for an unknown collection', () => {
+ const initialState = makeInitialState();
+ const state = reducer(initialState, toggleCollectionFileMode({ collectionUid: 'unknown' }));
+
+ expect(state.collections[0].fileMode).toBe(false);
+ });
+});
+
+describe('updateFileContent', () => {
+ test('creates a draft from the item and sets draft.raw', () => {
+ const state = reducer(
+ makeInitialState(),
+ updateFileContent({
+ collectionUid: COLLECTION_UID,
+ itemUid: ITEM_UID,
+ content: 'meta {\n name: user_info_edited\n}'
+ })
+ );
+
+ const item = state.collections[0].items[0];
+ expect(item.draft).not.toBeNull();
+ expect(item.draft.raw).toBe('meta {\n name: user_info_edited\n}');
+ // the draft preserves the structured request of the item
+ expect(item.draft.request).toEqual(item.request);
+ // the item itself is untouched
+ expect(item.raw).toBe('meta {\n name: user_info\n}');
+ });
+
+ test('updates raw on an existing draft without recreating it', () => {
+ let state = reducer(
+ makeInitialState(),
+ updateFileContent({ collectionUid: COLLECTION_UID, itemUid: ITEM_UID, content: 'edit one' })
+ );
+ const firstDraft = state.collections[0].items[0].draft;
+
+ state = reducer(
+ state,
+ updateFileContent({ collectionUid: COLLECTION_UID, itemUid: ITEM_UID, content: 'edit two' })
+ );
+
+ const item = state.collections[0].items[0];
+ expect(item.draft.raw).toBe('edit two');
+ expect(item.draft.request).toEqual(firstDraft.request);
+ });
+
+ test('does nothing for an unknown item', () => {
+ const state = reducer(
+ makeInitialState(),
+ updateFileContent({ collectionUid: COLLECTION_UID, itemUid: 'unknown', content: 'edited' })
+ );
+
+ expect(state.collections[0].items[0].draft).toBeNull();
+ });
+});
+
+describe('collectionChangeFileEvent — raw content', () => {
+ const makeFileEvent = ({ raw, request = makeRequest(), seq = 1 } = {}) => ({
+ file: {
+ meta: {
+ collectionUid: COLLECTION_UID,
+ pathname: '/coll/user_info.bru',
+ name: 'user_info.bru'
+ },
+ data: {
+ uid: ITEM_UID,
+ name: 'user_info',
+ type: 'http-request',
+ seq,
+ raw,
+ request
+ }
+ }
+ });
+
+ test('updates item.raw from the file event', () => {
+ const newRaw = 'meta {\n name: user_info_v2\n}';
+ const state = reducer(
+ makeInitialState(),
+ collectionChangeFileEvent(
+ makeFileEvent({ raw: newRaw, request: makeRequest({ url: 'https://example.com/v2' }) })
+ )
+ );
+
+ const item = state.collections[0].items[0];
+ expect(item.raw).toBe(newRaw);
+ expect(item.request.url).toBe('https://example.com/v2');
+ });
+
+ test('updates item.raw on a seq-only change', () => {
+ const newRaw = 'meta {\n name: user_info\n seq: 2\n}';
+ const state = reducer(
+ makeInitialState(),
+ collectionChangeFileEvent(makeFileEvent({ raw: newRaw, seq: 2 }))
+ );
+
+ const item = state.collections[0].items[0];
+ expect(item.seq).toBe(2);
+ expect(item.raw).toBe(newRaw);
+ });
+
+ test('clears the draft when draft.raw matches the file content (file-mode save round-trip)', () => {
+ let state = makeInitialState({ fileMode: true });
+ const editedRaw = 'meta {\n name: user_info\n}\n\nget {\n url: https://example.com/edited\n}';
+
+ state = reducer(
+ state,
+ updateFileContent({ collectionUid: COLLECTION_UID, itemUid: ITEM_UID, content: editedRaw })
+ );
+ expect(state.collections[0].items[0].draft).not.toBeNull();
+
+ // the file watcher reports the saved file back with the same raw content
+ state = reducer(
+ state,
+ collectionChangeFileEvent(
+ makeFileEvent({ raw: editedRaw, request: makeRequest({ url: 'https://example.com/edited' }) })
+ )
+ );
+
+ const item = state.collections[0].items[0];
+ expect(item.draft).toBeNull();
+ expect(item.raw).toBe(editedRaw);
+ });
+
+ test('preserves a draft whose raw content differs from the file content', () => {
+ let state = makeInitialState({ fileMode: true });
+
+ state = reducer(
+ state,
+ updateFileContent({ collectionUid: COLLECTION_UID, itemUid: ITEM_UID, content: 'unsaved edit' })
+ );
+
+ // a change arrives from disk that matches neither the draft structure nor its raw content
+ state = reducer(
+ state,
+ collectionChangeFileEvent(
+ makeFileEvent({
+ raw: 'meta {\n name: user_info_v3\n}',
+ request: makeRequest({ url: 'https://example.com/v3' })
+ })
+ )
+ );
+
+ const item = state.collections[0].items[0];
+ expect(item.raw).toBe('meta {\n name: user_info_v3\n}');
+ expect(item.draft).not.toBeNull();
+ expect(item.draft.raw).toBe('unsaved edit');
+ });
+
+ test('does not clear a genuine draft when raw is undefined on both the draft and the file event', () => {
+ // Simulate a structured-edit draft on an item that has no raw content,
+ // and a file change whose data also carries no raw. The undefined === undefined
+ // match must not wipe the user's unsaved edits.
+ let state = makeInitialState({ item: { raw: undefined } });
+
+ state.collections[0].items[0].draft = {
+ uid: ITEM_UID,
+ name: 'user_info',
+ type: 'http-request',
+ seq: 1,
+ request: makeRequest({ url: 'https://example.com/locally-edited' })
+ };
+
+ state = reducer(
+ state,
+ collectionChangeFileEvent({
+ file: {
+ meta: {
+ collectionUid: COLLECTION_UID,
+ pathname: '/coll/user_info.bru',
+ name: 'user_info.bru'
+ },
+ data: {
+ uid: ITEM_UID,
+ name: 'user_info',
+ type: 'http-request',
+ seq: 1,
+ request: makeRequest({ url: 'https://example.com/disk-change' })
+ }
+ }
+ })
+ );
+
+ const item = state.collections[0].items[0];
+ expect(item.draft).not.toBeNull();
+ expect(item.draft.request.url).toBe('https://example.com/locally-edited');
+ });
+});
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 c2f740f6a..ae93fd109 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
@@ -162,6 +162,7 @@ export const collectionsSlice = createSlice({
const collection = action.payload;
collection.settingsSelectedTab = 'overview';
+ collection.fileMode = false;
collection.folderLevelSettingsSelectedTab = {};
collection.allTags = []; // Initialize collection-level tags
@@ -2748,6 +2749,7 @@ export const collectionsSlice = createSlice({
currentItem.request = mergeRequestWithPreservedUids(currentItem.request, file.data.request);
currentItem.filename = file.meta.name;
currentItem.pathname = file.meta.pathname;
+ currentItem.raw = file.data.raw;
currentItem.settings = file.data.settings;
currentItem.examples = file.data.examples;
currentItem.draft = null;
@@ -2768,6 +2770,7 @@ export const collectionsSlice = createSlice({
examples: file.data.examples,
filename: file.meta.name,
pathname: file.meta.pathname,
+ raw: file.data.raw,
draft: null,
partial: file.partial,
loading: file.loading,
@@ -2853,6 +2856,7 @@ export const collectionsSlice = createSlice({
// we don't want to lose the draft in this case
if (areItemsTheSameExceptSeqUpdate(item, file.data)) {
item.seq = file.data.seq;
+ item.raw = file.data.raw;
if (item?.draft) {
item.draft.seq = file.data.seq;
}
@@ -2869,10 +2873,14 @@ export const collectionsSlice = createSlice({
item.examples = file.data.examples;
item.filename = file.meta.name;
item.pathname = file.meta.pathname;
+ item.raw = file.data.raw;
// Only clear draft if it matches the file content
// This preserves characters typed during autosave
- if (item.draft && areItemsTheSameExceptSeqUpdate(item.draft, file.data)) {
+ // The raw comparison is guarded so an undefined === undefined match
+ // (when neither side has raw content) does not wipe a genuine draft
+ const draftRawMatchesFile = item.draft?.raw !== undefined && item.draft.raw === file.data.raw;
+ if (item.draft && (areItemsTheSameExceptSeqUpdate(item.draft, file.data) || draftRawMatchesFile)) {
item.draft = null;
}
}
@@ -3294,6 +3302,27 @@ export const collectionsSlice = createSlice({
}
}
},
+ toggleCollectionFileMode: (state, action) => {
+ const { collectionUid } = action.payload;
+ const collection = findCollectionByUid(state.collections, collectionUid);
+ if (collection) {
+ collection.fileMode = !collection.fileMode;
+ }
+ },
+ updateFileContent: (state, action) => {
+ const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
+
+ if (collection) {
+ const item = findItemInCollection(collection, action.payload.itemUid);
+
+ if (item) {
+ if (!item.draft) {
+ item.draft = cloneDeep(item);
+ }
+ item.draft.raw = action.payload.content;
+ }
+ }
+ },
collectionAddOauth2CredentialsByUrl: (state, action) => {
const { collectionUid, folderUid, itemUid, url, credentials, credentialsId, debugInfo, executionMode } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
@@ -3819,6 +3848,8 @@ export const {
updateRunnerConfiguration,
updateRequestDocs,
updateFolderDocs,
+ toggleCollectionFileMode,
+ updateFileContent,
moveCollection,
streamDataReceived,
collectionAddOauth2CredentialsByUrl,
diff --git a/packages/bruno-electron/src/app/collection-watcher.js b/packages/bruno-electron/src/app/collection-watcher.js
index 308e825b5..d494c84b0 100644
--- a/packages/bruno-electron/src/app/collection-watcher.js
+++ b/packages/bruno-electron/src/app/collection-watcher.js
@@ -319,6 +319,7 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
file.partial = false;
file.loading = false;
file.size = sizeInMB(fileStats?.size);
+ file.data.raw = content;
hydrateRequestWithUuid(file.data, pathname);
win.webContents.send('main:collection-tree-updated', 'addFile', file);
} catch (error) {
@@ -360,6 +361,7 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
});
file.partial = false;
file.loading = false;
+ file.data.raw = content;
hydrateRequestWithUuid(file.data, pathname);
win.webContents.send('main:collection-tree-updated', 'addFile', file);
}
@@ -374,6 +376,7 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
file.partial = true;
file.loading = false;
file.size = sizeInMB(fileStats?.size);
+ file.data.raw = content;
hydrateRequestWithUuid(file.data, pathname);
win.webContents.send('main:collection-tree-updated', 'addFile', file);
} finally {
@@ -542,6 +545,7 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
file.data = await parseRequest(content, { format });
}
+ file.data.raw = content;
file.size = sizeInMB(fileStats?.size);
hydrateRequestWithUuid(file.data, pathname);
win.webContents.send('main:collection-tree-updated', 'change', file);
diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js
index 35408f051..fd8772f3d 100644
--- a/packages/bruno-electron/src/ipc/collection.js
+++ b/packages/bruno-electron/src/ipc/collection.js
@@ -624,6 +624,20 @@ const registerRendererEventHandlers = (mainWindow, watcher) => {
}
});
+ ipcMain.handle('renderer:save-file', async (event, pathname, content) => {
+ try {
+ validatePathIsInsideCollection(pathname);
+
+ if (!fs.existsSync(pathname)) {
+ throw new Error(`path: ${pathname} does not exist`);
+ }
+
+ await writeFile(pathname, content);
+ } catch (error) {
+ return Promise.reject(error);
+ }
+ });
+
// Helper: Parse file content based on scope type
const parseFileByType = async (fileContent, scopeType, format) => {
switch (scopeType) {
@@ -1720,6 +1734,16 @@ const registerRendererEventHandlers = (mainWindow, watcher) => {
}
});
+ ipcMain.handle('renderer:convert-to-json', async (event, item, content, format = 'bru') => {
+ try {
+ const jsonContent = await parseRequestViaWorker(content, { format });
+ const json = hydrateRequestWithUuid(jsonContent, item?.pathname);
+ return json;
+ } catch (error) {
+ return Promise.reject(error);
+ }
+ });
+
// add cookie
ipcMain.handle('renderer:add-cookie', async (event, domain, cookie) => {
try {