mirror of
https://github.com/usebruno/bruno.git
synced 2024-11-21 23:43:15 +01:00
[Feature] : Settings on folder level (#1334)
* feat(folder_settings): enable settings tab from folder, currently not using folder.bru * feat(folder_settings): read and write in folder settings only in headers, ignore folder.bru file il requests list * feat(folder_settings): merge collection and folder settings when sending network request * feat(folder_settings): remove console, testing headers merging working fine * feat(folder_settings): add missing endl for prettier check, remove redundant imports --------- Co-authored-by: Baptiste POULAIN <baptistepoulain@MAC882.local> Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
This commit is contained in:
parent
fc626041e2
commit
c1a57d30dc
@ -0,0 +1,56 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
}
|
||||
|
||||
thead {
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
font-size: 0.8125rem;
|
||||
user-select: none;
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-add-header {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
width: 100%;
|
||||
border: solid 1px transparent;
|
||||
outline: none !important;
|
||||
background-color: inherit;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
border: solid 1px transparent;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
@ -0,0 +1,150 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { addFolderHeader, updateFolderHeader, deleteFolderHeader } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
|
||||
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
|
||||
|
||||
const Headers = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const headers = get(folder, 'root.request.headers', []);
|
||||
|
||||
const addHeader = () => {
|
||||
dispatch(
|
||||
addFolderHeader({
|
||||
collectionUid: collection.uid,
|
||||
folderUid: folder.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveFolderRoot(collection.uid, folder.uid));
|
||||
const handleHeaderValueChange = (e, _header, type) => {
|
||||
const header = cloneDeep(_header);
|
||||
switch (type) {
|
||||
case 'name': {
|
||||
header.name = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'value': {
|
||||
header.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
header.enabled = e.target.checked;
|
||||
break;
|
||||
}
|
||||
}
|
||||
dispatch(
|
||||
updateFolderHeader({
|
||||
header: header,
|
||||
collectionUid: collection.uid,
|
||||
folderUid: folder.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveHeader = (header) => {
|
||||
dispatch(
|
||||
deleteFolderHeader({
|
||||
headerUid: header.uid,
|
||||
collectionUid: collection.uid,
|
||||
folderUid: folder.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{headers && headers.length
|
||||
? headers.map((header) => {
|
||||
return (
|
||||
<tr key={header.uid}>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
value={header.name}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(newValue) =>
|
||||
handleHeaderValueChange(
|
||||
{
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
header,
|
||||
'name'
|
||||
)
|
||||
}
|
||||
autocomplete={headerAutoCompleteList}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
value={header.value}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(newValue) =>
|
||||
handleHeaderValueChange(
|
||||
{
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
header,
|
||||
'value'
|
||||
)
|
||||
}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={header.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleHeaderValueChange(e, header, 'enabled')}
|
||||
/>
|
||||
<button tabIndex="-1" onClick={() => handleRemoveHeader(header)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
<button className="btn-add-header text-link pr-2 py-3 mt-2 select-none" onClick={addHeader}>
|
||||
+ Add Header
|
||||
</button>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default Headers;
|
52
packages/bruno-app/src/components/FolderSettings/index.js
Normal file
52
packages/bruno-app/src/components/FolderSettings/index.js
Normal file
@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { updateSettingsSelectedTab } from 'providers/ReduxStore/slices/collections';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Headers from './Headers';
|
||||
|
||||
const FolderSettings = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tab = folder?.settingsSelectedTab || 'headers';
|
||||
const setTab = (tab) => {
|
||||
dispatch(
|
||||
updateSettingsSelectedTab({
|
||||
collectionUid: folder.collectionUid,
|
||||
folderUid: folder.uid,
|
||||
tab
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'headers': {
|
||||
return <Headers collection={collection} folder={folder} />;
|
||||
}
|
||||
// TODO: Add auth
|
||||
}
|
||||
};
|
||||
|
||||
const getTabClassname = (tabName) => {
|
||||
return classnames(`tab select-none ${tabName}`, {
|
||||
active: tabName === tab
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full relative px-4 py-4">
|
||||
<div className="flex flex-wrap items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
{/* <div className={getTabClassname('auth')} role="tab" onClick={() => setTab('auth')}>
|
||||
Auth
|
||||
</div> */}
|
||||
</div>
|
||||
<section className={`flex ${['auth', 'script', 'docs', 'clientCert'].includes(tab) ? '' : 'mt-4'}`}>
|
||||
{getTabPanel(tab)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FolderSettings;
|
@ -18,6 +18,7 @@ import CollectionSettings from 'components/CollectionSettings';
|
||||
import { DocExplorer } from '@usebruno/graphql-docs';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import FolderSettings from 'components/FolderSettings';
|
||||
|
||||
const MIN_LEFT_PANE_WIDTH = 300;
|
||||
const MIN_RIGHT_PANE_WIDTH = 350;
|
||||
@ -131,6 +132,10 @@ const RequestTabPanel = () => {
|
||||
if (focusedTab.type === 'collection-settings') {
|
||||
return <CollectionSettings collection={collection} />;
|
||||
}
|
||||
if (focusedTab.type === 'folder-settings') {
|
||||
const folder = findItemInCollection(collection, focusedTab.folderUid);
|
||||
return <FolderSettings collection={collection} folder={folder} />;
|
||||
}
|
||||
|
||||
const item = findItemInCollection(collection, activeTabUid);
|
||||
if (!item || !item.uid) {
|
||||
|
@ -44,7 +44,7 @@ const CollectionToolBar = ({ collection }) => {
|
||||
<div className="flex items-center p-2">
|
||||
<div className="flex flex-1 items-center cursor-pointer hover:underline" onClick={viewCollectionSettings}>
|
||||
<IconFiles size={18} strokeWidth={1.5} />
|
||||
<span className="ml-2 mr-4 font-semibold">{collection.name}</span>
|
||||
<span className="ml-2 mr-4 font-semibold">{collection?.name || 'Folder'}</span>
|
||||
</div>
|
||||
<div className="flex flex-1 items-center justify-end">
|
||||
<span className="mr-2">
|
||||
|
@ -1,22 +1,30 @@
|
||||
import React from 'react';
|
||||
import { IconVariable, IconSettings, IconRun } from '@tabler/icons';
|
||||
|
||||
const SpecialTab = ({ handleCloseClick, type }) => {
|
||||
const getTabInfo = (type) => {
|
||||
const SpecialTab = ({ handleCloseClick, type, folderName }) => {
|
||||
const getTabInfo = (type, folderName) => {
|
||||
switch (type) {
|
||||
case 'collection-settings': {
|
||||
return (
|
||||
<>
|
||||
<IconSettings size={18} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<span className="ml-1">Collection</span>
|
||||
<span className="ml-1 leading-6">Collection</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'folder-settings': {
|
||||
return (
|
||||
<div className="flex items-center flex-nowrap overflow-hidden">
|
||||
<IconSettings size={18} strokeWidth={1.5} className="text-yellow-600 min-w-[18px]" />
|
||||
<span className="ml-1 leading-6 truncate">{folderName || 'Folder'}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case 'variables': {
|
||||
return (
|
||||
<>
|
||||
<IconVariable size={18} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<span className="ml-1">Variables</span>
|
||||
<span className="ml-1 leading-6">Variables</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -24,7 +32,7 @@ const SpecialTab = ({ handleCloseClick, type }) => {
|
||||
return (
|
||||
<>
|
||||
<IconRun size={18} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<span className="ml-1">Runner</span>
|
||||
<span className="ml-1 leading-6">Runner</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -33,7 +41,7 @@ const SpecialTab = ({ handleCloseClick, type }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center tab-label pl-2">{getTabInfo(type)}</div>
|
||||
<div className="flex items-center tab-label pl-2">{getTabInfo(type, folderName)}</div>
|
||||
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e)}>
|
||||
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
|
||||
<path
|
||||
|
@ -13,7 +13,7 @@ import RequestTabNotFound from './RequestTabNotFound';
|
||||
import SpecialTab from './SpecialTab';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const RequestTab = ({ tab, collection }) => {
|
||||
const RequestTab = ({ tab, collection, folderUid }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const [showConfirmClose, setShowConfirmClose] = useState(false);
|
||||
@ -80,11 +80,11 @@ const RequestTab = ({ tab, collection }) => {
|
||||
|
||||
return color;
|
||||
};
|
||||
|
||||
if (['collection-settings', 'variables', 'collection-runner'].includes(tab.type)) {
|
||||
const folder = folderUid ? findItemInCollection(collection, folderUid) : null;
|
||||
if (['collection-settings', 'folder-settings', 'variables', 'collection-runner'].includes(tab.type)) {
|
||||
return (
|
||||
<StyledWrapper className="flex items-center justify-between tab-container px-1">
|
||||
<SpecialTab handleCloseClick={handleCloseClick} type={tab.type} />
|
||||
<SpecialTab handleCloseClick={handleCloseClick} type={tab.type} folderName={folder?.name} />
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
@ -75,7 +75,6 @@ const RequestTabs = () => {
|
||||
'has-chevrons': showChevrons
|
||||
});
|
||||
};
|
||||
|
||||
// Todo: Must support ephemeral requests
|
||||
return (
|
||||
<StyledWrapper className={getRootClassname()}>
|
||||
@ -111,7 +110,7 @@ const RequestTabs = () => {
|
||||
role="tab"
|
||||
onClick={() => handleClick(tab)}
|
||||
>
|
||||
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} />
|
||||
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} folderUid={tab.folderUid} />
|
||||
</li>
|
||||
);
|
||||
})
|
||||
|
@ -24,6 +24,7 @@ import { hideHomePage } from 'providers/ReduxStore/slices/app';
|
||||
import toast from 'react-hot-toast';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import NetworkError from 'components/ResponsePane/NetworkError/index';
|
||||
import { uuid } from 'utils/common';
|
||||
|
||||
const CollectionItem = ({ item, collection, searchText }) => {
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
@ -188,6 +189,16 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
toast.error('URL is required');
|
||||
}
|
||||
};
|
||||
const viewFolderSettings = () => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: uuid(),
|
||||
collectionUid: collection.uid,
|
||||
folderUid: item.uid,
|
||||
type: 'folder-settings'
|
||||
})
|
||||
);
|
||||
};
|
||||
const requestItems = sortRequestItems(filter(item.items, (i) => isItemARequest(i)));
|
||||
const folderItems = sortFolderItems(filter(item.items, (i) => isItemAFolder(i)));
|
||||
|
||||
@ -345,6 +356,17 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
>
|
||||
Delete
|
||||
</div>
|
||||
{isFolder && (
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
viewFolderSettings();
|
||||
}}
|
||||
>
|
||||
Settings
|
||||
</div>
|
||||
)}
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
collectionUnlinkFileEvent,
|
||||
processEnvUpdateEvent,
|
||||
runFolderEvent,
|
||||
folderAddFileEvent,
|
||||
runRequestEvent,
|
||||
scriptEnvironmentUpdateEvent
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
@ -48,6 +49,13 @@ const useIpcEvents = () => {
|
||||
})
|
||||
);
|
||||
}
|
||||
if (type === 'addFileDir') {
|
||||
dispatch(
|
||||
folderAddFileEvent({
|
||||
file: val
|
||||
})
|
||||
);
|
||||
}
|
||||
if (type === 'change') {
|
||||
dispatch(
|
||||
collectionChangeFileEvent({
|
||||
|
@ -14,10 +14,11 @@ import {
|
||||
findParentItemInCollection,
|
||||
getItemsToResequence,
|
||||
isItemAFolder,
|
||||
refreshUidsInItem,
|
||||
findItemInCollectionByPathname,
|
||||
isItemARequest,
|
||||
moveCollectionItem,
|
||||
moveCollectionItemToRootOfCollection,
|
||||
refreshUidsInItem,
|
||||
transformRequestToSaveToFilesystem
|
||||
} from 'utils/collections';
|
||||
import { uuid, waitForNextTick } from 'utils/common';
|
||||
@ -143,7 +144,65 @@ export const saveCollectionRoot = (collectionUid) => (dispatch, getState) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const sendCollectionOauth2Request = (collectionUid) => (dispatch, getState) => {
|
||||
export const saveFolderRoot = (collectionUid, folderUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
const folder = findItemInCollection(collection, folderUid);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!collection) {
|
||||
return reject(new Error('Collection not found'));
|
||||
}
|
||||
|
||||
if (!folder) {
|
||||
return reject(new Error('Folder not found'));
|
||||
}
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:save-folder-root', folder.pathname, folder.root)
|
||||
.then(() => toast.success('Folder Settings saved successfully'))
|
||||
.then(resolve)
|
||||
.catch((err) => {
|
||||
toast.error('Failed to save folder settings!');
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const retrieveDirectoriesBetween = (pathname, parameter, filename) => {
|
||||
const parameterIndex = pathname.indexOf(parameter);
|
||||
const filenameIndex = pathname.indexOf(filename);
|
||||
if (parameterIndex === -1 || filenameIndex === -1 || filenameIndex < parameterIndex) {
|
||||
return [];
|
||||
}
|
||||
const directories = pathname
|
||||
.substring(parameterIndex + parameter.length, filenameIndex)
|
||||
.split('/')
|
||||
.filter((directory) => directory.trim() !== '');
|
||||
const reconstructedPaths = [];
|
||||
let currentPath = pathname.substring(0, parameterIndex + parameter.length);
|
||||
for (const directory of directories) {
|
||||
currentPath += `/${directory}`;
|
||||
reconstructedPaths.push(currentPath);
|
||||
}
|
||||
return reconstructedPaths;
|
||||
};
|
||||
|
||||
export const mergeRequests = (parentRequest, childRequest) => {
|
||||
return _.mergeWith({}, parentRequest, childRequest, customizer);
|
||||
};
|
||||
|
||||
function customizer(objValue, srcValue, key) {
|
||||
const exceptions = ['headers', 'params', 'vars'];
|
||||
if (exceptions.includes(key) && _.isArray(objValue) && _.isArray(srcValue)) {
|
||||
return _.unionBy(srcValue, objValue, 'name');
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const sendCollectionOauth2Request = (collectionUid, itemUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
|
||||
@ -156,7 +215,10 @@ export const sendCollectionOauth2Request = (collectionUid) => (dispatch, getStat
|
||||
|
||||
const environment = findEnvironmentInCollection(collectionCopy, collection.activeEnvironmentUid);
|
||||
|
||||
_sendCollectionOauth2Request(collection, environment, collectionCopy.collectionVariables)
|
||||
const externalSecrets = getExternalCollectionSecretsForActiveEnvironment({ collection });
|
||||
const secretVariables = getFormattedCollectionSecretVariables({ externalSecrets });
|
||||
|
||||
_sendCollectionOauth2Request(collection, environment, collectionCopy.collectionVariables, itemUid, secretVariables)
|
||||
.then((response) => {
|
||||
if (response?.data?.error) {
|
||||
toast.error(response?.data?.error);
|
||||
@ -184,9 +246,26 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
|
||||
const itemCopy = cloneDeep(item || {});
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
|
||||
const environment = findEnvironmentInCollection(collectionCopy, collection.activeEnvironmentUid);
|
||||
const environment = findEnvironmentInCollection(collectionCopy, collectionCopy.activeEnvironmentUid);
|
||||
const itemTree = retrieveDirectoriesBetween(itemCopy.pathname, collectionCopy.name, itemCopy.filename);
|
||||
|
||||
sendNetworkRequest(itemCopy, collection, environment, collectionCopy.collectionVariables)
|
||||
const folderDatas = itemTree.reduce((acc, currentPath) => {
|
||||
const folder = findItemInCollectionByPathname(collectionCopy, currentPath);
|
||||
if (folder) {
|
||||
acc = mergeRequests(acc, folder.root.request);
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
const mergeParams = mergeRequests(collectionCopy.root.request, folderDatas);
|
||||
// merge collection and folder settings with request
|
||||
const mergedCollection = {
|
||||
...collectionCopy,
|
||||
root: {
|
||||
...collectionCopy.root,
|
||||
request: mergeParams
|
||||
}
|
||||
};
|
||||
sendNetworkRequest(itemCopy, mergedCollection, environment, collectionCopy.collectionVariables)
|
||||
.then((response) => {
|
||||
return dispatch(
|
||||
responseReceived({
|
||||
|
@ -89,7 +89,7 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
},
|
||||
updateSettingsSelectedTab: (state, action) => {
|
||||
const { collectionUid, tab } = action.payload;
|
||||
const { collectionUid, folderUid, tab } = action.payload;
|
||||
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
@ -1114,6 +1114,44 @@ export const collectionsSlice = createSlice({
|
||||
set(collection, 'root.docs', action.payload.docs);
|
||||
}
|
||||
},
|
||||
addFolderHeader: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
if (folder) {
|
||||
const headers = get(folder, 'root.request.headers', []);
|
||||
headers.push({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
description: '',
|
||||
enabled: true
|
||||
});
|
||||
set(folder, 'root.request.headers', headers);
|
||||
}
|
||||
},
|
||||
updateFolderHeader: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
if (folder) {
|
||||
const headers = get(folder, 'root.request.headers', []);
|
||||
const header = find(headers, (h) => h.uid === action.payload.header.uid);
|
||||
if (header) {
|
||||
header.name = action.payload.header.name;
|
||||
header.value = action.payload.header.value;
|
||||
header.description = action.payload.header.description;
|
||||
header.enabled = action.payload.header.enabled;
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteFolderHeader: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
if (folder) {
|
||||
let headers = get(folder, 'root.request.headers', []);
|
||||
headers = filter(headers, (h) => h.uid !== action.payload.headerUid);
|
||||
set(folder, 'root.request.headers', headers);
|
||||
}
|
||||
},
|
||||
addCollectionHeader: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
@ -1152,11 +1190,22 @@ export const collectionsSlice = createSlice({
|
||||
set(collection, 'root.request.headers', headers);
|
||||
}
|
||||
},
|
||||
folderAddFileEvent: (state, action) => {
|
||||
const file = action.payload.file;
|
||||
const isFolderRoot = file.meta.folderRoot ? true : false;
|
||||
const collection = findCollectionByUid(state.collections, file.meta.collectionUid);
|
||||
const folder = findItemInCollectionByPathname(collection, file.meta.pathname);
|
||||
if (isFolderRoot) {
|
||||
if (folder) {
|
||||
folder.root = file.data;
|
||||
}
|
||||
return;
|
||||
}
|
||||
},
|
||||
collectionAddFileEvent: (state, action) => {
|
||||
const file = action.payload.file;
|
||||
const isCollectionRoot = file.meta.collectionRoot ? true : false;
|
||||
const collection = findCollectionByUid(state.collections, file.meta.collectionUid);
|
||||
|
||||
if (isCollectionRoot) {
|
||||
if (collection) {
|
||||
collection.root = file.data;
|
||||
@ -1187,7 +1236,7 @@ export const collectionsSlice = createSlice({
|
||||
currentSubItems = childItem.items;
|
||||
}
|
||||
|
||||
if (!currentSubItems.find((f) => f.name === file.meta.name)) {
|
||||
if (file.meta.name != 'folder.bru' && !currentSubItems.find((f) => f.name === file.meta.name)) {
|
||||
// this happens when you rename a file
|
||||
// the add event might get triggered first, before the unlink event
|
||||
// this results in duplicate uids causing react renderer to go mad
|
||||
@ -1521,6 +1570,9 @@ export const {
|
||||
addVar,
|
||||
updateVar,
|
||||
deleteVar,
|
||||
addFolderHeader,
|
||||
updateFolderHeader,
|
||||
deleteFolderHeader,
|
||||
addCollectionHeader,
|
||||
updateCollectionHeader,
|
||||
deleteCollectionHeader,
|
||||
@ -1537,6 +1589,7 @@ export const {
|
||||
collectionUnlinkDirectoryEvent,
|
||||
collectionAddEnvFileEvent,
|
||||
collectionRenamedEvent,
|
||||
folderAddFileEvent,
|
||||
resetRunResults,
|
||||
runRequestEvent,
|
||||
runFolderEvent,
|
||||
|
@ -38,7 +38,8 @@ export const tabsSlice = createSlice({
|
||||
requestPaneWidth: null,
|
||||
requestPaneTab: action.payload.requestPaneTab || 'params',
|
||||
responsePaneTab: 'response',
|
||||
type: action.payload.type || 'request'
|
||||
type: action.payload.type || 'request',
|
||||
...(action.payload.folderUid ? { folderUid: action.payload.folderUid } : {})
|
||||
});
|
||||
state.activeTabUid = action.payload.uid;
|
||||
},
|
||||
|
@ -179,6 +179,17 @@ const getCollectionRoot = (dir) => {
|
||||
return collectionBruToJson(content);
|
||||
};
|
||||
|
||||
const getFolderRoot = (dir) => {
|
||||
const folderRootPath = path.join(dir, 'folder.bru');
|
||||
const exists = fs.existsSync(folderRootPath);
|
||||
if (!exists) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(folderRootPath, 'utf8');
|
||||
return collectionBruToJson(content);
|
||||
};
|
||||
|
||||
const builder = async (yargs) => {
|
||||
yargs
|
||||
.option('r', {
|
||||
|
@ -40,10 +40,42 @@ const isBruEnvironmentConfig = (pathname, collectionPath) => {
|
||||
const isCollectionRootBruFile = (pathname, collectionPath) => {
|
||||
const dirname = path.dirname(pathname);
|
||||
const basename = path.basename(pathname);
|
||||
|
||||
return dirname === collectionPath && basename === 'collection.bru';
|
||||
};
|
||||
|
||||
const isFolderRootBruFile = (pathname, folderPath) => {
|
||||
const dirname = path.dirname(pathname);
|
||||
const basename = path.basename(pathname);
|
||||
return dirname === folderPath && basename === 'folder.bru';
|
||||
};
|
||||
|
||||
const scanDirectory = (directoryPath, callback) => {
|
||||
fs.readdir(directoryPath, (err, files) => {
|
||||
if (err) {
|
||||
console.error(`Error reading directory ${directoryPath}: ${err}`);
|
||||
return;
|
||||
}
|
||||
if (files.includes('folder.bru')) {
|
||||
callback(directoryPath);
|
||||
}
|
||||
// Iterate through each file/folder in the directory
|
||||
files.forEach((file) => {
|
||||
const filePath = path.join(directoryPath, file);
|
||||
// Check if it's a directory
|
||||
fs.stat(filePath, (err, stats) => {
|
||||
if (err) {
|
||||
console.error(`Error statting ${filePath}: ${err}`);
|
||||
return;
|
||||
}
|
||||
// If it's a directory, recursively scan it
|
||||
if (stats.isDirectory()) {
|
||||
scanDirectory(filePath, callback);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const hydrateRequestWithUuid = (request, pathname) => {
|
||||
request.uid = getRequestUid(pathname);
|
||||
|
||||
@ -225,12 +257,27 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
|
||||
collectionRoot: true
|
||||
}
|
||||
};
|
||||
|
||||
const folderCallback = (filePath) => {
|
||||
const bruContent = fs.readFileSync(`${filePath}/folder.bru`, 'utf8');
|
||||
if (bruContent) {
|
||||
const folder = {
|
||||
meta: {
|
||||
collectionUid,
|
||||
pathname: filePath,
|
||||
name: path.basename(filePath),
|
||||
folderRoot: true
|
||||
}
|
||||
};
|
||||
folder.data = collectionBruToJson(bruContent);
|
||||
hydrateBruCollectionFileWithUuid(folder.data);
|
||||
win.webContents.send('main:collection-tree-updated', 'addFileDir', folder);
|
||||
}
|
||||
};
|
||||
try {
|
||||
scanDirectory(collectionPath, folderCallback);
|
||||
let bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
|
||||
file.data = collectionBruToJson(bruContent);
|
||||
|
||||
hydrateBruCollectionFileWithUuid(file.data);
|
||||
win.webContents.send('main:collection-tree-updated', 'addFile', file);
|
||||
return;
|
||||
@ -334,7 +381,6 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
|
||||
let bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
|
||||
file.data = collectionBruToJson(bruContent);
|
||||
|
||||
hydrateBruCollectionFileWithUuid(file.data);
|
||||
win.webContents.send('main:collection-tree-updated', 'change', file);
|
||||
return;
|
||||
|
@ -152,6 +152,16 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:save-folder-root', async (event, folderPathname, folderRoot) => {
|
||||
try {
|
||||
const folderBruFilePath = path.join(folderPathname, 'folder.bru');
|
||||
|
||||
const content = jsonToBru(folderRoot);
|
||||
await writeFile(folderBruFilePath, content);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
ipcMain.handle('renderer:save-collection-root', async (event, collectionPathname, collectionRoot) => {
|
||||
try {
|
||||
const collectionBruFilePath = path.join(collectionPathname, 'collection.bru');
|
||||
|
Loading…
Reference in New Issue
Block a user