feat: September 2022 updates

This commit is contained in:
Anoop M D 2022-03-22 18:18:20 +05:30
parent 81b88e964f
commit c7cced5868
89 changed files with 3714 additions and 11804 deletions

View File

@ -1 +0,0 @@
{"type": "commonjs"}

22
LICENSE
View File

@ -1,22 +0,0 @@
MIT License
Copyright (c) 2021 Notebase Technologies LLP
Copyright (c) 2021 Anoop M D and Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,6 +1 @@
## development
```bash
# run this at root
lerna bootstrap --hoist
```

View File

@ -1,10 +0,0 @@
{
"version": "0.0.1",
"packages": [
"packages/*"
],
"publish": {
"message": "chore(release): publish",
"registry": "https://registry.npmjs.org"
}
}

55
main/app/menu-template.js Normal file
View File

@ -0,0 +1,55 @@
const { ipcMain } = require('electron');
const template = [
{
label: 'Collection',
submenu: [
{
label: 'Open Collection',
click () {
ipcMain.emit('main:open-collection');
}
},
{ role: 'quit' }
]
},
{
label: 'Edit',
submenu: [
{ role: 'undo'},
{ role: 'redo'},
{ role: 'separator'},
{ role: 'cut'},
{ role: 'copy'},
{ role: 'paste'}
]
},
{
label: 'View',
submenu: [
{ role: 'reload'},
{ role: 'toggledevtools'},
{ role: 'separator'},
{ role: 'resetzoom'},
{ role: 'zoomin'},
{ role: 'zoomout'},
{ role: 'separator'},
{ role: 'togglefullscreen'}
]
},
{
role: 'window',
submenu: [
{ role: 'minimize'},
{ role: 'close'}
]
},
{
role: 'help',
submenu: [
{ label: 'Learn More'}
]
}
];
module.exports = template;

View File

@ -1,75 +1,54 @@
// Native
const { join } = require('path');
const path = require('path');
const { format } = require('url');
const axios = require('axios');
const { BrowserWindow, app, Menu } = require('electron');
const { setContentSecurityPolicy } = require('electron-util');
// Packages
const { BrowserWindow, app, ipcMain } = require('electron');
const menuTemplate = require('./app/menu-template');
const registerIpc = require('./ipc');
const isDev = require('electron-is-dev');
const prepareNext = require('electron-next');
setContentSecurityPolicy(`
default-src * 'unsafe-inline' 'unsafe-eval';
script-src * 'unsafe-inline' 'unsafe-eval';
connect-src * 'unsafe-inline';
base-uri 'none';
form-action 'none';
frame-ancestors 'none';
`);
const menu = Menu.buildFromTemplate(menuTemplate);
Menu.setApplicationMenu(menu);
let mainWindow;
// Prepare the renderer once the app is ready
app.on('ready', async () => {
await prepareNext('./renderer');
const mainWindow = new BrowserWindow({
mainWindow = new BrowserWindow({
width: 1280,
height: 768,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
preload: join(__dirname, 'preload.js'),
contextIsolation: true,
preload: path.join(__dirname, "preload.js")
},
});
const url = isDev
? 'http://localhost:8000'
: format({
pathname: join(__dirname, '../renderer/out/index.html'),
pathname: path.join(__dirname, '../renderer/out/index.html'),
protocol: 'file:',
slashes: true,
})
slashes: true
});
mainWindow.loadURL(url);
// register all ipc handlers
registerIpc(mainWindow);
});
// Quit the app once all windows are closed
app.on('window-all-closed', app.quit);
// listen the channel `message` and resend the received message to the renderer process
ipcMain.on('message', (event, message) => {
event.sender.send('message', message);
});
// handler for all request related to a user's grafnode account
ipcMain.handle('grafnode-account-request', async (_, request) => {
const result = await axios(request)
return { data: result.data, status: result.status }
})
// handler for sending http request
ipcMain.handle('send-http-request', async (_, request) => {
try {
const result = await axios(request);
return {
status: result.status,
headers: result.headers,
data: result.data
};
} catch (error) {
if(error.response) {
return {
status: error.response.status,
headers: error.response.headers,
data: error.response.data
};
}
return {
status: -1,
headers: [],
data: null
};
}
})

33
main/ipc.js Normal file
View File

@ -0,0 +1,33 @@
const axios = require('axios');
const { ipcMain } = require('electron');
const registerIpc = () => {
// handler for sending http request
ipcMain.handle('send-http-request', async (event, request) => {
try {
const result = await axios(request);
return {
status: result.status,
headers: result.headers,
data: result.data
};
} catch (error) {
if(error.response) {
return {
status: error.response.status,
headers: error.response.headers,
data: error.response.data
};
}
return {
status: -1,
headers: [],
data: null
};
}
});
};
module.exports = registerIpc;

View File

@ -1,9 +1,14 @@
const { ipcRenderer, contextBridge } = require('electron');
contextBridge.exposeInMainWorld('electron', {
message: {
send: (payload) => ipcRenderer.send('message', payload),
on: (handler) => ipcRenderer.on('message', handler),
off: (handler) => ipcRenderer.off('message', handler),
contextBridge.exposeInMainWorld('ipcRenderer', {
invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
on: (channel, handler) => {
// Deliberately strip event as it includes `sender`
const subscription = (event, ...args) => handler(...args);
ipcRenderer.on(channel, subscription);
return () => {
ipcRenderer.removeListener(channel, subscription);
};
}
});

14
main/utils/common.js Normal file
View File

@ -0,0 +1,14 @@
const { customAlphabet } = require('nanoid');
// a customized version of nanoid without using _ and -
const uuid = () => {
// https://github.com/ai/nanoid/blob/main/url-alphabet/index.js
const urlAlphabet = 'useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict';
const customNanoId = customAlphabet (urlAlphabet, 21);
return customNanoId();
};
module.exports = {
uuid
};

12580
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -29,13 +29,18 @@
"codemirror-graphql": "^1.2.5",
"electron-is-dev": "^2.0.0",
"electron-next": "^3.1.5",
"electron-store": "^8.0.1",
"electron-util": "^0.17.2",
"escape-html": "^1.0.3",
"formik": "^2.2.9",
"fs-extra": "^10.0.1",
"graphiql": "^1.5.9",
"graphql": "^16.2.0",
"graphql-request": "^3.7.0",
"idb": "^7.0.0",
"immer": "^9.0.12",
"is-valid-path": "^0.1.1",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"markdown-it": "^12.2.0",
"mousetrap": "^1.6.5",
@ -46,6 +51,7 @@
"react-redux": "^7.2.6",
"react-tabs": "^3.2.3",
"sass": "^1.46.0",
"split-on-first": "^3.0.0",
"styled-components": "^5.3.3",
"tailwindcss": "^2.2.19",
"yup": "^0.32.11"

View File

@ -1,16 +1 @@
# grafnode
An opensource api collaboration platform.
## 👨‍💻 status
The project is still under active development.
## 🎯 goals
* ubiquitous - builds must be available for web, browser extensions and desktop apps
* self host - teams must be able to self host easily
* revision control - git like functionality for managing team collections
* data privacy - e2ee for hosted api collections
* minimalist - avoid feature bloat
* open source - all functionality released with [MIT](LICENSE) license
### License
[MIT](LICENSE)
# bruno

View File

@ -1,5 +1,6 @@
import { get, post, put } from './base';
// not used. kept as a placeholder for reference while implementing license key stuff
const AuthApi = {
whoami: () => get('auth/v1/user/whoami'),
signup: (params) => post('auth/v1/user/signup', params),
@ -15,11 +16,7 @@ const AuthApi = {
.then(resolve)
.catch(reject);
});
},
signout: () => post('auth/v1/user/logout'),
getProfile: () => get('auth/v1/user/profile'),
updateProfile: (params) => put('auth/v1/user/profile', params),
updateUsername: (params) => put('auth/v1/user/username', params)
}
};
export default AuthApi;

View File

@ -0,0 +1,30 @@
import React from 'react';
const Bruno = ({width}) => {
return (
<svg id="emoji" width={width} viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="color">
<path fill="#F4AA41" stroke="none" d="M23.5,14.5855l-4.5,1.75l-7.25,8.5l-4.5,10.75l2,5.25c1.2554,3.7911,3.5231,7.1832,7.25,10l2.5-3.3333 c0,0,3.8218,7.7098,10.7384,8.9598c0,0,10.2616,1.936,15.5949-0.8765c3.4203-1.8037,4.4167-4.4167,4.4167-4.4167l3.4167-3.4167 l1.5833,2.3333l2.0833-0.0833l5.4167-7.25L64,37.3355l-0.1667-4.5l-2.3333-5.5l-4.8333-7.4167c0,0-2.6667-4.9167-8.1667-3.9167 c0,0-6.5-4.8333-11.8333-4.0833S32.0833,10.6688,23.5,14.5855z"/>
<polygon fill="#EA5A47" stroke="none" points="36,47.2521 32.9167,49.6688 30.4167,49.6688 30.3333,53.5021 31.0833,57.0021 32.1667,58.9188 35,60.4188 39.5833,59.8355 41.1667,58.0855 42.1667,53.8355 41.9167,49.8355 39.9167,50.0855"/>
<polygon fill="#3F3F3F" stroke="none" points="32.5,36.9188 30.9167,40.6688 33.0833,41.9188 34.3333,42.4188 38.6667,42.5855 41.5833,40.3355 39.8333,37.0855"/>
</g>
<g id="hair"/>
<g id="skin"/>
<g id="skin-shadow"/>
<g id="line">
<path fill="#000000" stroke="none" d="M29.5059,30.1088c0,0-1.8051,1.2424-2.7484,0.6679c-0.9434-0.5745-1.2424-1.8051-0.6679-2.7484 s1.805-1.2424,2.7484-0.6679S29.5059,30.1088,29.5059,30.1088z"/>
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M33.1089,37.006h6.1457c0.4011,0,0.7634,0.2397,0.9203,0.6089l1.1579,2.7245l-2.1792,1.1456 c-0.6156,0.3236-1.3654-0.0645-1.4567-0.754"/>
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M34.7606,40.763c-0.1132,0.6268-0.7757,0.9895-1.3647,0.7471l-2.3132-0.952l1.0899-2.9035 c0.1465-0.3901,0.5195-0.6486,0.9362-0.6486"/>
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M30.4364,50.0268c0,0-0.7187,8.7934,3.0072,9.9375c2.6459,0.8125,5.1497,0.5324,6.0625-0.25 c0.875-0.75,2.6323-4.4741,1.8267-9.6875"/>
<path fill="#000000" stroke="none" d="M44.2636,30.1088c0,0,1.805,1.2424,2.7484,0.6679c0.9434-0.5745,1.2424-1.8051,0.6679-2.7484 c-0.5745-0.9434-1.805-1.2424-2.7484-0.6679C43.9881,27.9349,44.2636,30.1088,44.2636,30.1088z"/>
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M25.6245,42.8393c-0.475,3.6024,2.2343,5.7505,4.2847,6.8414c1.1968,0.6367,2.6508,0.5182,3.7176-0.3181l2.581-2.0233l2.581,2.0233 c1.0669,0.8363,2.5209,0.9548,3.7176,0.3181c2.0504-1.0909,4.7597-3.239,4.2847-6.8414"/>
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M19.9509,28.3572c-2.3166,5.1597-0.5084,13.0249,0.119,15.3759c0.122,0.4571,0.0755,0.9355-0.1271,1.3631l-1.9874,4.1937 c-0.623,1.3146-2.3934,1.5533-3.331,0.4409c-3.1921-3.7871-8.5584-11.3899-6.5486-16.686 c7.0625-18.6104,15.8677-18.1429,15.8677-18.1429c2.8453-1.9336,13.1042-6.9375,24.8125,0.875c0,0,8.6323-1.7175,14.9375,16.9375 c1.8036,5.3362-3.4297,12.8668-6.5506,16.6442c-0.9312,1.127-2.7162,0.8939-3.3423-0.4272l-1.9741-4.1656 c-0.2026-0.4275-0.2491-0.906-0.1271-1.3631c0.6275-2.3509,2.4356-10.2161,0.119-15.3759"/>
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M52.6309,46.4628c0,0-3.0781,6.7216-7.8049,8.2712"/>
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M19.437,46.969c0,0,3.0781,6.0823,7.8049,7.632"/>
<line x1="36.2078" x2="36.2078" y1="47.3393" y2="44.3093" fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2"/>
</g>
</svg>
)
};
export default Bruno;

View File

@ -41,25 +41,27 @@ export default class QueryEditor extends React.Component {
readOnly: this.props.readOnly ? 'nocursor' : false,
extraKeys: {
'Cmd-Enter': () => {
if (this.props.onChange) {
// empty
if(this.props.onRun) {
this.props.onRun();
}
},
'Ctrl-Enter': () => {
if (this.props.onChange) {
// empty
if(this.props.onRun) {
this.props.onRun();
}
},
'Cmd-S': () => {
if (this.props.onRunQuery) {
// empty
if(this.props.onSave) {
this.props.onSave();
}
},
'Ctrl-S': () => {
if (this.props.onRunQuery) {
// empty
if(this.props.onSave) {
this.props.onSave();
}
},
'Tab': function(cm){
cm.replaceSelection(" " , "end");
}
},
}));

View File

@ -8,7 +8,7 @@ const EnvironmentSelector = () => {
const Icon = forwardRef((props, ref) => {
return (
<div ref={ref} className="current-enviroment flex items-center justify-center px-3 py-1 select-none">
<div ref={ref} className="current-enviroment flex items-center justify-center pl-3 pr-2 py-1 select-none">
No Environment
<IconCaretDown className="caret" size={14} strokeWidth={2}/>
</div>
@ -19,7 +19,7 @@ const EnvironmentSelector = () => {
return (
<StyledWrapper>
<div className="flex items-center cursor-pointer environment-selector pr-3">
<div className="flex items-center cursor-pointer environment-selector">
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement='bottom-end'>
<div className="dropdown-item" onClick={() => {
dropdownTippyRef.current.hide();

View File

@ -4,12 +4,12 @@ const Wrapper = styled.div`
&.modal--animate-out{
animation: fade-out 0.5s forwards cubic-bezier(.19,1,.22,1);
.grafnode-modal-card {
.bruno-modal-card {
animation: fade-and-slide-out-from-top .50s forwards cubic-bezier(.19,1,.22,1);
}
}
&.grafnode-modal {
&.bruno-modal {
position: fixed;
top: 0;
left: 0;
@ -22,7 +22,7 @@ const Wrapper = styled.div`
z-index: 1003;
}
.grafnode-modal-card {
.bruno-modal-card {
animation-duration: .85s;
animation-delay: .1s;
background: var(--color-background-top);
@ -57,7 +57,7 @@ const Wrapper = styled.div`
animation: fade-and-slide-in-from-top .50s forwards cubic-bezier(.19,1,.22,1);
}
.grafnode-modal-header {
.bruno-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
@ -84,12 +84,12 @@ const Wrapper = styled.div`
}
}
.grafnode-modal-content {
.bruno-modal-content {
flex-grow: 1;
background-color: #fff;
}
.grafnode-modal-backdrop {
.bruno-modal-backdrop {
height: 100%;
width: 100%;
left: 0;
@ -112,7 +112,7 @@ const Wrapper = styled.div`
animation: fade-in .1s forwards cubic-bezier(.19,1,.22,1);
}
.grafnode-modal-footer {
.bruno-modal-footer {
background-color: white;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;

View File

@ -2,8 +2,8 @@ import React, {useState, useEffect} from 'react';
import StyledWrapper from './StyledWrapper';
const ModalHeader = ({title, handleCancel}) => (
<div className="grafnode-modal-header">
{title ? <div className="grafnode-modal-heade-title">{title}</div> : null}
<div className="bruno-modal-header">
{title ? <div className="bruno-modal-heade-title">{title}</div> : null}
{handleCancel ? (
<div className="close cursor-pointer" onClick={handleCancel ? () => handleCancel() : null}>
×
@ -13,7 +13,7 @@ const ModalHeader = ({title, handleCancel}) => (
);
const ModalContent = ({children}) => (
<div className="grafnode-modal-content px-4 py-6">
<div className="bruno-modal-content px-4 py-6">
{children}
</div>
);
@ -23,7 +23,7 @@ const ModalFooter = ({confirmText, cancelText, handleSubmit, handleCancel, confi
cancelText = cancelText || 'Cancel';
return (
<div className="flex justify-end p-4 grafnode-modal-footer">
<div className="flex justify-end p-4 bruno-modal-footer">
<span className="mr-2">
<button type="button" onClick={handleCancel} className="btn btn-md btn-close">
{cancelText}
@ -69,13 +69,13 @@ const Modal = ({
}
}, []);
let classes = 'grafnode-modal';
let classes = 'bruno-modal';
if (isClosing) {
classes += ' modal--animate-out';
}
return (
<StyledWrapper className={classes}>
<div className={`grafnode-modal-card modal-${size}`}>
<div className={`bruno-modal-card modal-${size}`}>
<ModalHeader title={title} handleCancel={() => closeModal()} />
<ModalContent>{children}</ModalContent>
<ModalFooter
@ -86,7 +86,7 @@ const Modal = ({
confirmDisabled={confirmDisabled}
/>
</div>
<div className="grafnode-modal-backdrop" onClick={() => closeModal()} />
<div className="bruno-modal-backdrop" onClick={() => closeModal()} />
</StyledWrapper>
);
};

View File

@ -1,58 +1,26 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.react-tabs__tab-list {
border-bottom: none !important;
padding-top: 0;
padding-left: 0 !important;
display: flex;
align-items: center;
margin: 0;
.react-tabs__tab {
div.tabs {
div.tab {
padding: 6px 0px;
border: none;
user-select: none;
border-bottom: solid 2px transparent;
margin-right: 24px;
color: rgb(125 125 125);
outline: none !important;
margin-right: 1.25rem;
color: var(--color-tab-inactive);
cursor: pointer;
&:focus, &:active, &:focus-within, &:focus-visible, &:target {
outline: none !important;
box-shadow: none !important;
}
&:after {
display: none !important;
&.active {
color: #322e2c !important;
border-bottom: solid 2px var(--color-tab-active-border) !important;
}
}
}
.react-tabs__tab--selected {
border: none;
color: #322e2c !important;
border-bottom: solid 2px var(--color-tab-active-border) !important;
border-color: var(--color-tab-active-border) !important;
background: inherit;
outline: none !important;
box-shadow: none !important;
&:focus, &:active, &:focus-within, &:focus-visible, &:target {
border: none;
outline: none !important;
box-shadow: none !important;
border-bottom: solid 2px var(--color-tab-active-border) !important;
border-color: var(--color-tab-active-border) !important;
background: inherit;
outline: none !important;
box-shadow: none !important;
}
}
.react-tabs__tab-panel--selected {
height: 90%;
}
`;
export default StyledWrapper;

View File

@ -1,33 +1,79 @@
import React from 'react';
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
import find from 'lodash/find';
import classnames from 'classnames';
import { useSelector, useDispatch } from 'react-redux';
import { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs';
import QueryParams from 'components/RequestPane/QueryParams';
import RequestHeaders from 'components/RequestPane/RequestHeaders';
import RequestBody from 'components/RequestPane/RequestBody';
import RequestBodyMode from 'components/RequestPane/RequestBody/RequestBodyMode';
import StyledWrapper from './StyledWrapper';
const HttpRequestPane = ({item, collection, leftPaneWidth}) => {
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const selectTab = (tab) => {
dispatch(updateRequestPaneTab({
uid: item.uid,
requestPaneTab: tab
}))
};
const getTabPanel = (tab) => {
switch(tab) {
case 'params': {
return <QueryParams item={item} collection={collection}/>;
}
case 'body': {
return <RequestBody item={item} collection={collection}/>;
}
case 'headers': {
return <RequestHeaders item={item} collection={collection}/>;
}
default: {
return <div className="mt-4">404 | Not found</div>;
}
}
}
if(!activeTabUid) {
return (
<div>Something went wrong</div>
);
}
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
if(!focusedTab || !focusedTab.uid || !focusedTab.requestPaneTab) {
return (
<div className="pb-4 px-4">An error occured!</div>
);
}
const getTabClassname = (tabName) => {
return classnames(`tab select-none ${tabName}`, {
'active': tabName === focusedTab.requestPaneTab
});
};
return (
<StyledWrapper className="h-full">
<Tabs className='react-tabs mt-1 flex flex-grow flex-col h-full'>
<TabList>
<Tab tabIndex="-1">Params</Tab>
<Tab tabIndex="-1">Body</Tab>
<Tab tabIndex="-1">Headers</Tab>
<Tab tabIndex="-1">Auth</Tab>
</TabList>
<TabPanel>
<QueryParams />
</TabPanel>
<TabPanel>
<RequestBody item={item} collection={collection}/>
</TabPanel>
<TabPanel>
<RequestHeaders item={item} collection={collection}/>
</TabPanel>
<TabPanel>
<div>Auth</div>
</TabPanel>
</Tabs>
<StyledWrapper className="flex flex-col h-full relativ">
<div className="flex items-center tabs" role="tablist">
<div className={getTabClassname('params')} role="tab" onClick={() => selectTab('params')}>Params</div>
<div className={getTabClassname('body')} role="tab" onClick={() => selectTab('body')}>Body</div>
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>Headers</div>
{/* Moved to post mvp */}
{/* <div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>Auth</div> */}
{focusedTab.requestPaneTab === 'body' ? (
<div className="flex flex-grow justify-end items-center">
<RequestBodyMode />
</div>
) : null }
</div>
<section className="flex w-full mt-5">
{getTabPanel(focusedTab.requestPaneTab)}
</section>
</StyledWrapper>
)
};

View File

@ -4,7 +4,7 @@ const StyledWrapper = styled.div`
div.CodeMirror {
border: solid 1px var(--color-codemirror-border);
/* todo: find a better way */
height: calc(100vh - 255px);
height: calc(100vh - 250px);
}
textarea.cm-editor {

View File

@ -13,6 +13,7 @@ const Wrapper = styled.div`
thead {
color: #616161;
font-size: 0.8125rem;
user-select: none;
}
td {
padding: 6px 10px;
@ -20,8 +21,10 @@ const Wrapper = styled.div`
}
.btn-add-param {
margin-block: 10px;
padding: 5px;
font-size: 0.8125rem;
&:hover span {
text-decoration: underline;
}
}
input[type="text"] {

View File

@ -1,40 +1,62 @@
import React, { useState } from 'react';
import { nanoid } from 'nanoid';
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 { addQueryParam, updateQueryParam, deleteQueryParam } from 'providers/ReduxStore/slices/collections';
import StyledWrapper from './StyledWrapper';
const initialState = [{
uid: nanoid(),
enabled: true
}];
const QueryParams = ({item, collection}) => {
const dispatch = useDispatch();
const params = item.draft ? get(item, 'draft.request.params') : get(item, 'request.params');
const QueryParams = () => {
const [params, setParams] = useState(initialState);
const addParam = () => {
let newParam = {
uid: nanoid(),
key: '',
value: '',
description: '',
enabled: true
};
let newParams = [...params, newParam];
setParams(newParams);
const handleAddParam = () => {
dispatch(addQueryParam({
itemUid: item.uid,
collectionUid: collection.uid,
}));
};
const handleParamValueChange = (e, index, menu) => {
// todo: yet to implement
const handleParamChange = (e, _param, type) => {
const param = cloneDeep(_param);
switch(type) {
case 'name' : {
param.name = e.target.value;
break;
}
case 'value' : {
param.value = e.target.value;
break;
}
case 'description' : {
param.description = e.target.value;
break;
}
case 'enabled' : {
param.enabled = e.target.checked;
break;
}
}
dispatch(updateQueryParam({
param,
itemUid: item.uid,
collectionUid: collection.uid
}));
};
const handleRemoveHeader = (index) => {
params.splice(index, 1);
setParams([...params]);
const handleRemoveParam = (param) => {
dispatch(deleteQueryParam({
paramUid: param.uid,
itemUid: item.uid,
collectionUid: collection.uid
}));
};
return (
<StyledWrapper className="mt-4">
<StyledWrapper className="w-full">
<table>
<thead>
<tr>
@ -45,46 +67,45 @@ const QueryParams = () => {
</tr>
</thead>
<tbody>
{params && params.length ? params.map((header, index) => {
{params && params.length ? params.map((param, index) => {
return (
<tr key={header.uid}>
<tr key={param.uid}>
<td>
<input
type="text"
name="key"
autoComplete="off"
defaultValue={params[index].key}
onChange={(e) => handleParamValueChange(e, index, 'key')}
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={param.name}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'name')}
/>
</td>
<td>
<input
type="text"
name="value"
autoComplete="off"
defaultValue={params[index].value}
onChange={(e) => handleParamValueChange(e, index, 'value')}
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={param.value}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'value')}
/>
</td>
<td>
<input
type="text"
name="description"
autoComplete="off"
defaultValue={params[index].description}
onChange={(e) => handleParamValueChange(e, index, 'description')}
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={param.description}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'description')}
/>
</td>
<td>
<div className="flex items-center">
<input
type="checkbox"
className="mr-3"
defaultChecked={header.enabled}
name="enabled"
onChange={(e) => handleParamValueChange(e, index, 'enabled')}
checked={param.enabled}
className="mr-3 mousetrap"
onChange={(e) => handleParamChange(e, param, 'enabled')}
/>
<button onClick={() => handleRemoveHeader(index)}>
<button onClick={() => handleRemoveParam(param)}>
<IconTrash strokeWidth={1.5} size={20}/>
</button>
</div>
@ -94,7 +115,9 @@ const QueryParams = () => {
}) : null}
</tbody>
</table>
<button className="btn-add-param" onClick={addParam}>+ Add Param</button>
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={handleAddParam}>
+&nbsp;<span>Add Param</span>
</button>
</StyledWrapper>
)
};

View File

@ -0,0 +1,30 @@
import styled from 'styled-components';
const Wrapper = styled.div`
font-size: 0.8125rem;
.dropdown {
width: 100%;
}
.method-selector {
border-radius: 3px;
min-width: 90px;
.tippy-box {
max-width: 150px !important;
min-width: 110px !important;
}
.dropdown-item {
padding: .25rem .6rem !important;
}
}
.caret {
color: rgb(140, 140, 140);
fill: rgb(140 140 140);
}
`;
export default Wrapper;

View File

@ -0,0 +1,48 @@
import React, { useRef, forwardRef } from 'react';
import { IconCaretDown } from '@tabler/icons';
import Dropdown from 'components/Dropdown';
import StyledWrapper from './StyledWrapper';
const HttpMethodSelector = ({method, onMethodSelect}) => {
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => dropdownTippyRef.current = ref;
const Icon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex w-full items-center pl-3 py-1 select-none uppercase">
<div className="flex-grow font-medium">{method}</div>
<div><IconCaretDown className="caret ml-2 mr-2" size={14} strokeWidth={2}/></div>
</div>
);
});
const handleMethodSelect = (verb) => onMethodSelect(verb);
const Verb = ({verb}) => {
return (
<div className="dropdown-item" onClick={() => {
dropdownTippyRef.current.hide();
handleMethodSelect(verb);
}}>
{verb}
</div>
);
};
return(
<StyledWrapper>
<div className="flex items-center cursor-pointer method-selector">
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement='bottom-start'>
<Verb verb='GET' />
<Verb verb='POST' />
<Verb verb='PUT' />
<Verb verb='DELETE' />
<Verb verb='PATCH' />
<Verb verb='OPTIONS' />
<Verb verb='HEAD' />
</Dropdown>
</div>
</StyledWrapper>
);
};
export default HttpMethodSelector;

View File

@ -3,7 +3,7 @@ import styled from 'styled-components';
const Wrapper = styled.div`
height: 2.3rem;
div.method-selector {
div.method-selector-container {
border: solid 1px var(--color-layout-border);
border-right: none;
background-color: var(--color-sidebar-background);

View File

@ -1,91 +1,42 @@
import React, { useRef, forwardRef } from 'react';
import PropTypes from 'prop-types';
import { IconCaretDown } from '@tabler/icons';
import Dropdown from 'components/Dropdown';
import SaveRequestButton from 'components/RequestPane/SaveRequest';
import React from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import { requestUrlChanged, updateRequestMethod } from 'providers/ReduxStore/slices/collections';
import HttpMethodSelector from './HttpMethodSelector';
import StyledWrapper from './StyledWrapper';
const QueryUrl = ({value, onChange, handleRun, collections}) => {
const dropdownTippyRef = useRef();
const viewProfile = () => {};
const QueryUrl = ({item, collection, handleRun}) => {
const dispatch = useDispatch();
const method = item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');
let url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
const Icon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex items-center justify-center pl-3 py-2 select-none">
GET <IconCaretDown className="caret ml-2 mr-1" size={14} strokeWidth={2}/>
</div>
);
});
const onUrlChange = (value) => {
dispatch(requestUrlChanged({
itemUid: item.uid,
collectionUid: collection.uid,
url: value
}));
};
const onDropdownCreate = (ref) => dropdownTippyRef.current = ref;
const onMethodSelect = (verb) => {
dispatch(updateRequestMethod({
method: verb,
itemUid: item.uid,
collectionUid: collection.uid
}));
};
return (
<StyledWrapper className="mt-3 flex items-center">
<div className="flex items-center cursor-pointer user-action-dropdown h-full method-selector pr-3">
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement='bottom-start'>
<div>
<div className="dropdown-item" onClick={() => {
dropdownTippyRef.current.hide();
viewProfile();
}}>
GET
</div>
</div>
<div>
<div className="dropdown-item" onClick={() => {
dropdownTippyRef.current.hide();
viewProfile();
}}>
POST
</div>
</div>
<div>
<div className="dropdown-item" onClick={() => {
dropdownTippyRef.current.hide();
viewProfile();
}}>
PUT
</div>
</div>
<div>
<div className="dropdown-item" onClick={() => {
dropdownTippyRef.current.hide();
viewProfile();
}}>
DELETE
</div>
</div>
<div>
<div className="dropdown-item" onClick={() => {
dropdownTippyRef.current.hide();
viewProfile();
}}>
PATCH
</div>
</div>
<div>
<div className="dropdown-item" onClick={() => {
dropdownTippyRef.current.hide();
viewProfile();
}}>
OPTIONS
</div>
</div>
<div>
<div className="dropdown-item" onClick={() => {
dropdownTippyRef.current.hide();
viewProfile();
}}>
HEAD
</div>
</div>
</Dropdown>
<StyledWrapper className="flex items-center">
<div className="flex items-center h-full method-selector-container">
<HttpMethodSelector method={method} onMethodSelect={onMethodSelect}/>
</div>
<div className="flex items-center flex-grow input-container h-full">
<input
className="px-3 w-full mousetrap"
type="text" defaultValue={value}
onChange={(event) => onChange(event.target.value)}
type="text" value={url}
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
onChange={(event) => onUrlChange(event.target.value)}
/>
</div>
<button
@ -95,15 +46,8 @@ const QueryUrl = ({value, onChange, handleRun, collections}) => {
>
<span style={{marginLeft: 5}}>Send</span>
</button>
<SaveRequestButton folders={collections}/>
</StyledWrapper>
)
};
QueryUrl.propTypes = {
value: PropTypes.string,
onChange: PropTypes.func.isRequired,
handleRun: PropTypes.func.isRequired
};
export default QueryUrl;

View File

@ -18,7 +18,7 @@ const RequestBodyMode = () => {
return(
<StyledWrapper>
<div className="inline-flex items-center cursor-pointer body-mode-selector">
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement='bottom-start'>
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement='bottom-end'>
<div className="label-item font-medium">
Form
</div>

View File

@ -1,6 +1,10 @@
import styled from 'styled-components';
const Wrapper = styled.div`
div.CodeMirror {
/* todo: find a better way */
height: calc(100vh - 240px);
}
`;
export default Wrapper;

View File

@ -2,9 +2,7 @@ import React from 'react';
import get from 'lodash/get';
import CodeEditor from 'components/CodeEditor';
import { useDispatch } from 'react-redux';
import { requestChanged } from 'providers/ReduxStore/slices/tabs';
import { updateRequestBody } from 'providers/ReduxStore/slices/collections';
import RequestBodyMode from './RequestBodyMode';
import { updateRequestBody, sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections';
import StyledWrapper from './StyledWrapper';
const RequestBody = ({item, collection}) => {
@ -12,10 +10,6 @@ const RequestBody = ({item, collection}) => {
const bodyContent = item.draft ? get(item, 'draft.request.body.content') : get(item, 'request.body.content');
const onEdit = (value) => {
dispatch(requestChanged({
itemUid: item.uid,
collectionUid: collection.uid
}));
dispatch(updateRequestBody({
mode: 'json',
content: value,
@ -24,12 +18,12 @@ const RequestBody = ({item, collection}) => {
}));
};
const onRun = () => dispatch(sendRequest(item, collection.uid));;
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));;
return(
<StyledWrapper className="mt-3">
<RequestBodyMode />
<div className="mt-4">
<CodeEditor value={bodyContent || ''} onEdit={onEdit}/>
</div>
<StyledWrapper className="w-full">
<CodeEditor value={bodyContent || ''} onEdit={onEdit} onRun={onRun} onSave={onSave}/>
</StyledWrapper>
);
};

View File

@ -13,6 +13,7 @@ const Wrapper = styled.div`
thead {
color: #616161;
font-size: 0.8125rem;
user-select: none;
}
td {
padding: 6px 10px;
@ -20,6 +21,7 @@ const Wrapper = styled.div`
}
.btn-add-header {
font-size: 0.8125rem;
margin-block: 10px;
padding: 5px;
}

View File

@ -3,7 +3,6 @@ import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { requestChanged } from 'providers/ReduxStore/slices/tabs';
import { addRequestHeader, updateRequestHeader, deleteRequestHeader } from 'providers/ReduxStore/slices/collections';
import StyledWrapper from './StyledWrapper';
@ -12,10 +11,6 @@ const RequestHeaders = ({item, collection}) => {
const headers = item.draft ? get(item, 'draft.request.headers') : get(item, 'request.headers');
const addHeader = () => {
dispatch(requestChanged({
itemUid: item.uid,
collectionUid: collection.uid
}));
dispatch(addRequestHeader({
itemUid: item.uid,
collectionUid: collection.uid,
@ -37,11 +32,11 @@ const RequestHeaders = ({item, collection}) => {
header.description = e.target.value;
break;
}
case 'enabled' : {
header.enabled = e.target.checked;
break;
}
}
dispatch(requestChanged({
itemUid: item.uid,
collectionUid: collection.uid
}));
dispatch(updateRequestHeader({
header: header,
itemUid: item.uid,
@ -50,10 +45,6 @@ const RequestHeaders = ({item, collection}) => {
};
const handleRemoveHeader = (header) => {
dispatch(requestChanged({
itemUid: item.uid,
collectionUid: collection.uid
}));
dispatch(deleteRequestHeader({
headerUid: header.uid,
itemUid: item.uid,
@ -62,7 +53,7 @@ const RequestHeaders = ({item, collection}) => {
};
return (
<StyledWrapper className="mt-4">
<StyledWrapper className="w-full">
<table>
<thead>
<tr>
@ -79,7 +70,7 @@ const RequestHeaders = ({item, collection}) => {
<td>
<input
type="text"
autoComplete="off"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={header.name}
className="mousetrap"
onChange={(e) => handleHeaderValueChange(e, header, 'name')}
@ -88,7 +79,7 @@ const RequestHeaders = ({item, collection}) => {
<td>
<input
type="text"
autoComplete="off"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={header.value}
className="mousetrap"
onChange={(e) => handleHeaderValueChange(e, header, 'value')}
@ -97,7 +88,7 @@ const RequestHeaders = ({item, collection}) => {
<td>
<input
type="text"
autoComplete="off"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={header.description}
className="mousetrap"
onChange={(e) => handleHeaderValueChange(e, header, 'description')}
@ -121,7 +112,7 @@ const RequestHeaders = ({item, collection}) => {
}) : null}
</tbody>
</table>
<button className="btn-add-header" onClick={addHeader}>+ Add Header</button>
<button className="btn-add-header select-none" onClick={addHeader}>+ Add Header</button>
</StyledWrapper>
)
};

View File

@ -1,8 +1,6 @@
import styled from 'styled-components';
const Wrapper = styled.div`
height: 2.3rem;
.folder-list {
border: 1px solid #ccc;
border-radius: 5px;
@ -11,7 +9,7 @@ const Wrapper = styled.div`
padding-block: 8px;
padding-inline: 12px;
cursor: pointer;
&: hover {
&:hover {
background-color: #e8e8e8;
}
}

View File

@ -4,61 +4,54 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import StyledWrapper from './StyledWrapper';
import Modal from 'components//Modal';
const SaveRequestButton = ({folders}) => {
const [openSaveRequestModal, setOpenSaveRequestModal] = useState(false);
const SaveRequest = ({items, onClose}) => {
const [showFolders, setShowFolders] = useState([]);
useEffect(() => {
setShowFolders(folders);
}, [folders, openSaveRequestModal])
setShowFolders(items || []);
}, [items])
const handleFolderClick = (folder) => {
let subFolders = [];
for (let item of folder.items) {
if (item.items) {
subFolders.push(item)
if(folder.items && folder.items.length) {
for (let item of folder.items) {
if (item.items) {
subFolders.push(item)
}
}
if(subFolders.length) {
setShowFolders(subFolders);
}
}
subFolders.length ? setShowFolders(subFolders) : setShowFolders((prev) => prev);
}
return (
<StyledWrapper className="flex items-center">
<button
style={{backgroundColor: 'var(--color-brand)'}}
className="flex items-center h-full text-white active:bg-blue-600 font-bold text-xs px-4 py-2 ml-2 uppercase rounded shadow hover:shadow-md outline-none focus:outline-none ease-linear transition-all duration-150"
onClick={() => {
setOpenSaveRequestModal(true);
}}
<StyledWrapper>
<Modal
size ="md"
title ="Save Request"
confirmText ="Save"
cancelText ="Cancel"
handleCancel = {onClose}
handleConfirm = {onClose}
>
<span style={{marginLeft: 5}}>Save</span>
</button>
{openSaveRequestModal ? (
<Modal
size ="md"
title ="save request"
confirmText ="Save"
cancelText ="Cancel"
handleCancel = {() => setOpenSaveRequestModal(false)}
handleConfirm = {() => setOpenSaveRequestModal(false)}
>
<p className="mb-2">Select a folder to save request:</p>
<div className="folder-list">
{showFolders && showFolders.length ? showFolders.map((folder) => (
<div
key={folder.uid}
className="folder-name"
onClick={() => handleFolderClick(folder)}
>
<FontAwesomeIcon className="mr-3 text-gray-500" icon={faFolder} style={{fontSize: 20}}/>
{folder.name}
</div>
)): null}
</div>
</Modal>
): null}
<p className="mb-2">Select a folder to save request:</p>
<div className="folder-list">
{showFolders && showFolders.length ? showFolders.map((folder) => (
<div
key={folder.uid}
className="folder-name"
onClick={() => handleFolderClick(folder)}
>
<FontAwesomeIcon className="mr-3 text-gray-500" icon={faFolder} style={{fontSize: 20}}/>
{folder.name}
</div>
)): null}
</div>
</Modal>
</StyledWrapper>
)
);
};
export default SaveRequestButton;
export default SaveRequest;

View File

@ -0,0 +1,29 @@
import React from 'react';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { useDispatch } from 'react-redux';
const RequestNotFound = ({itemUid}) => {
const dispatch = useDispatch();
const closeTab = () => {
dispatch(closeTabs({
tabUids: [itemUid]
}));
};
return (
<div className="mt-6 px-6">
<div className="p-4 bg-orange-100 border-l-4 border-yellow-500 text-yellow-700 bg-yellow-100 p-4">
<div>Request no longer exists.</div>
<div className="mt-2">
This can happen when the yml file associated with this request was deleted on your filesystem.
</div>
</div>
<button className="btn btn-md btn-secondary mt-6" onClick={closeTab}>
Close Tab
</button>
</div>
);
};
export default RequestNotFound;

View File

@ -1,15 +1,27 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
&.dragging {
cursor: col-resize;
}
div.drag-request {
display: flex;
width: 0.5rem;
align-items: center;
justify-content: center;
width: 10px;
padding: 0;
cursor: col-resize;
background: transparent;
border-left: solid 1px var(--color-request-dragbar-background);
&:hover {
div.drag-request-border {
display: flex;
height: 100%;
width: 1px;
border-left: solid 1px var(--color-request-dragbar-background);
}
&:hover div.drag-request-border {
border-left: solid 1px var(--color-request-dragbar-background-active);
}
}

View File

@ -7,8 +7,9 @@ import HttpRequestPane from 'components/RequestPane/HttpRequestPane';
import ResponsePane from 'components/ResponsePane';
import Welcome from 'components/Welcome';
import { findItemInCollection } from 'utils/collections';
import { sendRequest, requestUrlChanged } from 'providers/ReduxStore/slices/collections';
import { requestChanged } from 'providers/ReduxStore/slices/tabs';
import { sendRequest } from 'providers/ReduxStore/slices/collections';
import { updateRequestPaneTabWidth } from 'providers/ReduxStore/slices/tabs';
import RequestNotFound from './RequestNotFound';
import useGraphqlSchema from '../../hooks/useGraphqlSchema';
import StyledWrapper from './StyledWrapper';
@ -17,29 +18,42 @@ const RequestTabPanel = () => {
if(typeof window == 'undefined') {
return <div></div>;
}
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const collections = useSelector((state) => state.collections.collections);
const dispatch = useDispatch();
const screenWidth = useSelector((state) => state.app.screenWidth);
let asideWidth = useSelector((state) => state.app.leftSidebarWidth);
let {
schema
} = useGraphqlSchema('https://api.spacex.land/graphql');
const [leftPaneWidth, setLeftPaneWidth] = useState((window.innerWidth - asideWidth)/2 - 10); // 10 is for dragbar
const [rightPaneWidth, setRightPaneWidth] = useState((window.innerWidth - asideWidth)/2);
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
const [leftPaneWidth, setLeftPaneWidth] = useState(focusedTab && focusedTab.requestPaneWidth ? focusedTab.requestPaneWidth : ((screenWidth - asideWidth)/2.2)); // 2.2 so that request pane is relatively smaller
const [rightPaneWidth, setRightPaneWidth] = useState(screenWidth - asideWidth - leftPaneWidth - 5);
const [dragging, setDragging] = useState(false);
useEffect(() => {
const leftPaneWidth = (screenWidth - asideWidth)/2.2;
setLeftPaneWidth(leftPaneWidth);
}, [screenWidth]);
useEffect(() => {
setRightPaneWidth(screenWidth - asideWidth - leftPaneWidth - 5);
}, [screenWidth, asideWidth, leftPaneWidth]);
const handleMouseMove = (e) => {
if(dragging) {
e.preventDefault();
setLeftPaneWidth(e.clientX - asideWidth);
setRightPaneWidth(window.innerWidth - (e.clientX));
setLeftPaneWidth(e.clientX - asideWidth - 5);
setRightPaneWidth(screenWidth - (e.clientX) - 5);
}
};
const handleMouseUp = (e) => {
if(dragging) {
e.preventDefault();
setDragging(false);
dispatch(updateRequestPaneTabWidth({
uid: activeTabUid,
requestPaneWidth: e.clientX - asideWidth - 5
}));
}
};
const handleDragbarMouseDown = (e) => {
@ -47,6 +61,10 @@ const RequestTabPanel = () => {
setDragging(true);
};
let {
schema
} = useGraphqlSchema('https://api.spacex.land/graphql');
useEffect(() => {
document.addEventListener('mouseup', handleMouseUp);
document.addEventListener('mousemove', handleMouseMove);
@ -55,11 +73,8 @@ const RequestTabPanel = () => {
document.removeEventListener('mouseup', handleMouseUp);
document.removeEventListener('mousemove', handleMouseMove);
};
}, [dragging, leftPaneWidth]);
}, [dragging, asideWidth]);
const onGraphqlQueryChange = (value) => {
// todo
};
if(!activeTabUid) {
return (
@ -67,8 +82,6 @@ const RequestTabPanel = () => {
);
}
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
if(!focusedTab || !focusedTab.uid || !focusedTab.collectionUid) {
return (
<div className="pb-4 px-4">An error occured!</div>
@ -85,50 +98,30 @@ const RequestTabPanel = () => {
const item = findItemInCollection(collection, activeTabUid);
if(!item || !item.uid) {
return (
<StyledWrapper>
Request not found!
</StyledWrapper>
<RequestNotFound itemUid={activeTabUid}/>
);
}
const onUrlChange = (value) => {
dispatch(requestChanged({
itemUid: item.uid,
collectionUid: collection.uid
}));
dispatch(requestUrlChanged({
itemUid: item.uid,
collectionUid: collection.uid,
url: value
}));
};
const runQuery = async () => {
// todo
};
const sendNetworkRequest = async () => {
dispatch(sendRequest(item, collection.uid));
};
const onGraphqlQueryChange = (value) => {};
const runQuery = async () => {};
const sendNetworkRequest = async () => dispatch(sendRequest(item, collection.uid));
return (
<StyledWrapper className="flex flex-col flex-grow">
<StyledWrapper className={`flex flex-col flex-grow ${dragging ? 'dragging' : ''}`}>
<div
className="pb-4 px-4"
className="px-4 pt-6 pb-4"
style={{
borderBottom: 'solid 1px var(--color-layout-border)'
borderBottom: 'solid 1px var(--color-request-dragbar-background)'
}}
>
<div className="pt-1 text-gray-600">{item.name}</div>
<QueryUrl
value = {item.request.url}
onChange={onUrlChange}
item = {item}
collection={collection}
handleRun={sendNetworkRequest}
collections={collections}
/>
</div>
<section className="main flex flex-grow">
<section className="request-pane">
<section className="request-pane mt-2">
<div
className="px-4"
style={{width: `${leftPaneWidth}px`, height: 'calc(100% - 5px)'}}
@ -154,10 +147,12 @@ const RequestTabPanel = () => {
</section>
<div className="drag-request" onMouseDown={handleDragbarMouseDown}>
<div className="drag-request-border" />
</div>
<section className="response-pane flex-grow">
<section className="response-pane flex-grow mt-2">
<ResponsePane
item={item}
rightPaneWidth={rightPaneWidth}
response={item.response}
isLoading={item.response && item.response.state === 'sending' ? true : false}

View File

@ -1,5 +1,5 @@
import React from 'react';
import { IconStack, IconGitFork } from '@tabler/icons';
import { IconFolders } from '@tabler/icons';
import EnvironmentSelector from 'components/EnvironmentSelector';
import StyledWrapper from './StyledWrapper';
@ -8,10 +8,8 @@ const CollectionToolBar = ({collection}) => {
<StyledWrapper>
<div className="flex items-center p-2">
<div className="flex flex-1 items-center">
<IconStack size={18} strokeWidth={1.5}/>
<span className="ml-2 mr-4 font-semibold">anoop<span style={{paddingInline: 2}}>/</span>{collection.name}</span>
<IconGitFork size={16} strokeWidth={1}/>
<span className="ml-1 text-xs">from anoop/notebase</span>
<IconFolders size={18} strokeWidth={1.5}/>
<span className="ml-2 mr-4 font-semibold">{collection.name}</span>
</div>
<div className="flex flex-1 items-center justify-end">
<EnvironmentSelector />

View File

@ -0,0 +1,38 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.tab-label {
overflow: hidden;
}
.tab-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.close-icon-container {
min-height: 20px;
border-radius: 3px;
.close-icon {
display: none;
color: #9f9f9f;
width: 8px;
padding-bottom: 6px;
padding-top: 6px;
}
&:hover, &:hover .close-icon {
background-color: #eaeaea;
color: rgb(76 76 76);
}
.has-changes-icon {
height: 24px;
}
}
`;
export default StyledWrapper;

View File

@ -0,0 +1,78 @@
import React from 'react';
import get from 'lodash/get';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { useDispatch } from 'react-redux';
import { findItemInCollection } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
import { IconAlertTriangle } from '@tabler/icons';
const RequestTab = ({tab, collection}) => {
const dispatch = useDispatch();
const handleCloseClick = (event) => {
event.stopPropagation();
event.preventDefault();
dispatch(closeTabs({
tabUids: [tab.uid]
}))
};
const getMethodColor = (method = '') => {
let color = '';
method = method.toLocaleLowerCase();
switch(method) {
case 'get': {
color = 'rgb(5, 150, 105)';
break;
}
case 'post': {
color = '#8e44ad';
break;
}
}
return color;
};
const item = findItemInCollection(collection, tab.uid);
if(!item) {
return (
<StyledWrapper className="flex items-center justify-between tab-container px-1">
<div className="flex items-center tab-label pl-2">
<IconAlertTriangle size={18} strokeWidth={1.5} className="text-yellow-600"/>
<span className="ml-1">Not Found</span>
</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 fill="currentColor" d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"></path>
</svg>
</div>
</StyledWrapper>
);
}
const method = item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');
return (
<StyledWrapper className="flex items-center justify-between tab-container px-1">
<div className="flex items-center tab-label pl-2">
<span className="tab-method uppercase" style={{color: getMethodColor(method)}}>{method}</span>
<span className="text-gray-700 ml-1 tab-name" title={item.name}>{item.name}</span>
</div>
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e)}>
{!item.draft ? (
<svg focusable="false"xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
<path fill="currentColor" d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"></path>
</svg>
) : (
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" width="8" height="16" fill="#cc7b1b" className="has-changes-icon" viewBox="0 0 8 8">
<circle cx="4" cy="4" r="3"/>
</svg>
) }
</div>
</StyledWrapper>
);
};
export default RequestTab;

View File

@ -1,20 +1,27 @@
import styled from 'styled-components';
const Wrapper = styled.div`
border-bottom: 1px solid var(--color-layout-border);
ul {
width: 100%;
padding: 0;
margin: 0 0 10px;
padding-left: 1rem;
border-bottom: 1px solid var(--color-layout-border);
margin: 0;
display: flex;
bottom: -1px;
position: relative;
overflow: scroll;
&::-webkit-scrollbar {
display: none;
}
li {
display: inline-flex;
width: 150px;
min-width: 150px;
max-width: 150px;
border: 1px solid transparent;
border-bottom: none;
bottom: -1px;
position: relative;
list-style: none;
padding-top: 8px;
padding-bottom: 8px;
@ -40,38 +47,6 @@ const Wrapper = styled.div`
}
}
.tab-label {
overflow: hidden;
}
.tab-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; }
.close-icon-container {
min-height: 20px;
border-radius: 3px;
.close-icon {
display: none;
color: #9f9f9f;
width: 8px;
padding-bottom: 6px;
padding-top: 6px;
}
&:hover, &:hover .close-icon {
background-color: #eaeaea;
color: rgb(76 76 76);
}
.has-changes-icon {
height: 24px;
}
}
&.active {
.close-icon-container .close-icon {
display: block;
@ -84,14 +59,17 @@ const Wrapper = styled.div`
}
}
&.new-tab {
&.short-tab {
vertical-align: bottom;
width: 34px;
min-width: 34px;
max-width: 34px;
padding: 3px 0px;
display: inline-flex;
justify-content: center;
color: rgb(117 117 117);
margin-bottom: 2px;
position: relative;
top: -1px;
> div {
padding: 3px 4px;
@ -145,6 +123,16 @@ const Wrapper = styled.div`
}
}
}
&.has-chevrons {
ul {
li:first-child {
.tab-container {
border-left: 1px solid #dcdcdc;
}
}
}
}
`;
export default Wrapper;

View File

@ -1,19 +1,24 @@
import React from 'react';
import React, { useState, useRef } from 'react';
import find from 'lodash/find';
import filter from 'lodash/filter';
import classnames from 'classnames';
import { IconHome2 } from '@tabler/icons';
import { IconHome2, IconChevronRight, IconChevronLeft } from '@tabler/icons';
import { useSelector, useDispatch } from 'react-redux';
import { focusTab, closeTab } from 'providers/ReduxStore/slices/tabs';
import { findItemInCollection } from 'utils/collections';
import { focusTab } from 'providers/ReduxStore/slices/tabs';
import NewRequest from 'components/Sidebar/NewRequest';
import CollectionToolBar from './CollectionToolBar';
import RequestTab from './RequestTab';
import StyledWrapper from './StyledWrapper';
const RequestTabs = () => {
const dispatch = useDispatch();
const tabsRef = useRef();
const [newRequestModalOpen, setNewRequestModalOpen] = useState(false);
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const collections = useSelector((state) => state.collections.collections);
const dispatch = useDispatch();
const leftSidebarWidth = useSelector((state) => state.app.leftSidebarWidth);
const screenWidth = useSelector((state) => state.app.screenWidth);
const getTabClassname = (tab, index) => {
return classnames("request-tab select-none", {
@ -22,39 +27,13 @@ const RequestTabs = () => {
});
};
const getMethodColor = (method) => {
let color = '';
switch(method) {
case 'GET': {
color = 'rgb(5, 150, 105)';
break;
}
case 'POST': {
color = '#8e44ad';
break;
}
}
return color;
};
const handleClick = (tab) => {
dispatch(focusTab({
uid: tab.uid
}));
};
const handleCloseClick = (event, tab) => {
event.stopPropagation();
event.preventDefault();
dispatch(closeTab({
tabUid: tab.uid
}))
};
const createNewTab = () => {
// todo
};
const createNewTab = () => setNewRequestModalOpen(true);
if(!activeTabUid) {
return null;
@ -71,73 +50,89 @@ const RequestTabs = () => {
const activeCollection = find(collections, (c) => c.uid === activeTab.collectionUid);
const collectionRequestTabs = filter(tabs, (t) => t.collectionUid === activeTab.collectionUid);
const item = findItemInCollection(activeCollection, activeTab.uid);
if(!item || !item.uid) {
return (
<StyledWrapper>
Request not found!
</StyledWrapper>
);
}
const maxTablistWidth = screenWidth - leftSidebarWidth - 150;
const tabsWidth = (collectionRequestTabs.length * 150) + 34; // 34: (+)icon
const showChevrons = maxTablistWidth < tabsWidth;
const getRequestName = (tab) => {
const item = findItemInCollection(activeCollection, tab.uid);
return item ? item.name : '';
}
const leftSlide = () => {
tabsRef.current.scrollBy({
left: -120,
behavior: 'smooth'
});
};
const getRequestMethod = (tab) => {
const item = findItemInCollection(activeCollection, tab.uid);
return item ? item.request.method : '';
}
// todo: bring new tab to focus if its not in focus
// tabsRef.current.scrollLeft
const rightSlide = () => {
tabsRef.current.scrollBy({
left: 120,
behavior: 'smooth'
});
};
const getRootClassname = () => {
return classnames({
'has-chevrons': showChevrons
});
};
return (
<StyledWrapper>
<StyledWrapper className={getRootClassname()}>
{newRequestModalOpen && <NewRequest isEphermal={true} collection={activeCollection} onClose={() => setNewRequestModalOpen(false)}/>}
{collectionRequestTabs && collectionRequestTabs.length ? (
<>
<CollectionToolBar collection={activeCollection}/>
<div className="flex items-center">
<div className="flex items-center pl-4">
<ul role="tablist">
<li className="select-none new-tab mr-1" onClick={createNewTab}>
{showChevrons ? (
<li className="select-none short-tab" onClick={leftSlide}>
<div className="flex items-center">
<IconChevronLeft size={18} strokeWidth={1.5}/>
</div>
</li>
) : null}
{/* Moved to post mvp */}
{/* <li className="select-none new-tab mr-1" onClick={createNewTab}>
<div className="flex items-center home-icon-container">
<IconHome2 size={18} strokeWidth={1.5}/>
</div>
</li>
</li> */}
</ul>
<ul role="tablist" style={{maxWidth: maxTablistWidth}} ref={tabsRef}>
{collectionRequestTabs && collectionRequestTabs.length ? collectionRequestTabs.map((tab, index) => {
return <li key={tab.uid} className={getTabClassname(tab, index)} role="tab" onClick={() => handleClick(tab)}>
<div className="flex items-center justify-between tab-container px-1">
<div className="flex items-center tab-label pl-2">
<span className="tab-method" style={{color: getMethodColor(getRequestMethod(tab))}}>{getRequestMethod(tab)}</span>
<span className="text-gray-700 ml-1 tab-name">{getRequestName(tab)}</span>
</div>
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e, tab)}>
{!tab.hasChanges ? (
<svg focusable="false"xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
<path fill="currentColor" d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"></path>
</svg>
) : (
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" width="8" height="16" fill="#cc7b1b" className="has-changes-icon" viewBox="0 0 8 8">
<circle cx="4" cy="4" r="3"/>
</svg>
) }
</div>
return (
<li key={tab.uid} className={getTabClassname(tab, index)} role="tab" onClick={() => handleClick(tab)}>
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} activeTab={activeTab}/>
</li>
)
}) : null}
</ul>
<ul role="tablist">
{showChevrons ? (
<li className="select-none short-tab" onClick={rightSlide}>
<div className="flex items-center">
<IconChevronRight size={18} strokeWidth={1.5}/>
</div>
</li>
}) : null}
<li className="select-none new-tab ml-1" onClick={createNewTab}>
) : null}
<li className={`select-none short-tab ${showChevrons ? '' : 'ml-1'}`} onClick={createNewTab}>
<div className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
</svg>
</div>
</li>
<li className="select-none new-tab choose-request">
{/* Moved to post mvp */}
{/* <li className="select-none new-tab choose-request">
<div className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
<path d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/>
</svg>
</div>
</li>
</li> */}
</ul>
</div>
</>

View File

@ -3,7 +3,7 @@ import styled from 'styled-components';
const StyledWrapper = styled.div`
div.CodeMirror {
/* todo: find a better way */
height: calc(100vh - 255px);
height: calc(100vh - 240px);
}
`;

View File

@ -4,9 +4,9 @@ import StyledWrapper from './StyledWrapper';
const QueryResult = ({value, width}) => {
return (
<StyledWrapper className="mt-4 px-3 w-full" style={{maxWidth: width}}>
<StyledWrapper className="px-3 w-full" style={{maxWidth: width}}>
<div className="h-full">
<CodeEditor value={value || ''} />
<CodeEditor value={value || ''} readOnly/>
</div>
</StyledWrapper>
);

View File

@ -3,7 +3,7 @@ import StyledWrapper from './StyledWrapper';
const ResponseHeaders = ({headers}) => {
return (
<StyledWrapper className="mt-3 px-3 pb-4 w-full">
<StyledWrapper className="px-3 pb-4 w-full">
<table>
<thead>
<tr>

View File

@ -13,7 +13,7 @@ const ResponseSize = ({size}) => {
}
return (
<StyledWrapper className="mt-3 ml-4">
<StyledWrapper className="ml-4">
{sizeToDisplay}
</StyledWrapper>
)

View File

@ -13,7 +13,7 @@ const ResponseTime = ({duration}) => {
}
return (
<StyledWrapper className="mt-3 ml-4">
<StyledWrapper className="ml-4">
{durationToDisplay}
</StyledWrapper>
)

View File

@ -5,7 +5,7 @@ import StyledWrapper from './StyledWrapper';
const StatusCode = ({status}) => {
const getTabClassname = () => {
return classnames('mt-3', {
return classnames('', {
'text-blue-700': status >= 100 && status < 200,
'text-green-700': status >= 200 && status < 300,
'text-purple-700': status >= 300 && status < 400,

View File

@ -6,8 +6,8 @@ const StyledWrapper = styled.div`
padding: 6px 0px;
border: none;
border-bottom: solid 2px transparent;
margin-right: 20px;
color: rgb(125 125 125);
margin-right: 1.25rem;
color: var(--color-tab-inactive);
cursor: pointer;
&:focus, &:active, &:focus-within, &:focus-visible, &:target {

View File

@ -1,5 +1,8 @@
import React, { useState } from 'react';
import React from 'react';
import find from 'lodash/find';
import classnames from 'classnames';
import { useSelector, useDispatch } from 'react-redux';
import { updateResponsePaneTab } from 'providers/ReduxStore/slices/tabs';
import QueryResult from './QueryResult';
import Overlay from './Overlay';
import Placeholder from './Placeholder';
@ -9,17 +12,20 @@ import ResponseTime from './ResponseTime';
import ResponseSize from './ResponseSize';
import StyledWrapper from './StyledWrapper';
const ResponsePane = ({rightPaneWidth, response, isLoading}) => {
const [selectedTab, setSelectedTab] = useState('response');
const ResponsePane = ({rightPaneWidth, item, isLoading}) => {
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
response = response || {};
const getTabClassname = (tabName) => {
return classnames(`tab select-none ${tabName}`, {
'active': tabName === selectedTab
});
const selectTab = (tab) => {
dispatch(updateResponsePaneTab({
uid: item.uid,
responsePaneTab: tab
}))
};
const response = item.response || {};
const getTabPanel = (tab) => {
switch(tab) {
case 'response': {
@ -40,7 +46,7 @@ const ResponsePane = ({rightPaneWidth, response, isLoading}) => {
return <div>404 | Not found</div>;
}
}
}
};
if(isLoading) {
return (
@ -49,7 +55,6 @@ const ResponsePane = ({rightPaneWidth, response, isLoading}) => {
</StyledWrapper>
);
}
if(response.state !== 'success') {
return (
<StyledWrapper className="flex h-full relative">
@ -58,11 +63,30 @@ const ResponsePane = ({rightPaneWidth, response, isLoading}) => {
);
}
if(!activeTabUid) {
return (
<div>Something went wrong</div>
);
}
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
if(!focusedTab || !focusedTab.uid || !focusedTab.responsePaneTab) {
return (
<div className="pb-4 px-4">An error occured!</div>
);
}
const getTabClassname = (tabName) => {
return classnames(`tab select-none ${tabName}`, {
'active': tabName === focusedTab.responsePaneTab
});
};
return (
<StyledWrapper className="flex flex-col h-full relative">
<div className="flex items-center px-3 tabs mt-1" role="tablist">
<div className={getTabClassname('response')} role="tab" onClick={() => setSelectedTab('response')}>Response</div>
<div className={getTabClassname('headers')} role="tab" onClick={() => setSelectedTab('headers')}>Headers</div>
<div className="flex items-center px-3 tabs" role="tablist">
<div className={getTabClassname('response')} role="tab" onClick={() => selectTab('response')}>Response</div>
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>Headers</div>
{!isLoading ? (
<div className="flex flex-grow justify-end items-center">
<StatusCode status={response.status}/>
@ -71,8 +95,8 @@ const ResponsePane = ({rightPaneWidth, response, isLoading}) => {
</div>
) : null }
</div>
<section className="flex flex-grow">
{getTabPanel(selectedTab)}
<section className="flex flex-grow mt-5">
{getTabPanel(focusedTab.responsePaneTab)}
</section>
</StyledWrapper>
)

View File

@ -2,18 +2,27 @@ import React from 'react';
import Modal from 'components/Modal';
import { isItemAFolder } from 'utils/tabs';
import { useDispatch } from 'react-redux';
import { closeTab } from 'providers/ReduxStore/slices/tabs';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { deleteItem } from 'providers/ReduxStore/slices/collections';
import { recursivelyGetAllItemUids } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
const DeleteCollectionItem = ({onClose, item, collection}) => {
const dispatch = useDispatch();
const isFolder = isItemAFolder(item);
const onConfirm = () =>{
dispatch(closeTab({
tabUid: item.uid
}));
dispatch(deleteItem(item.uid, collection.uid));
dispatch(deleteItem(item.uid, collection.uid))
.then(() => {
if(isFolder) {
dispatch(closeTabs({
tabUids: recursivelyGetAllItemUids(item.items)
}));
} else {
dispatch(closeTabs({
tabUids: [item.uid]
}));
}
});
onClose();
};

View File

@ -43,13 +43,14 @@ const RenameCollectionItem = ({collection, item, onClose}) => {
handleConfirm={onSubmit}
handleCancel={onClose}
>
<form className="grafnode-form" onSubmit={formik.handleSubmit}>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div>
<label htmlFor="name" className="block font-semibold">{isFolder ? 'Folder' : 'Request'} Name</label>
<input
id="collection-item-name" type="text" name="name"
ref={inputRef}
className="block textbox mt-2 w-full"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
onChange={formik.handleChange}
value={formik.values.name || ''}
/>

View File

@ -21,7 +21,7 @@ const RequestMethod = ({item}) => {
return (
<StyledWrapper>
<div className={getClassname(item.request.method)}>
<span>{item.request.method}</span>
<span className="uppercase">{item.request.method}</span>
</div>
</StyledWrapper>
);

View File

@ -2,9 +2,16 @@ import styled from 'styled-components';
const Wrapper = styled.div`
.menu-icon {
display: none;
flex-grow: 1;
justify-content: flex-end;
color: rgb(110 110 110);
.dropdown {
div[aria-expanded="true"] {
visibility: visible;
}
div[aria-expanded="false"] {
visibility: hidden;
}
}
}
.indent-block {
@ -20,17 +27,23 @@ const Wrapper = styled.div`
transform: rotateZ(90deg);
}
span.item-name {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
&:hover {
background: #e7e7e7;
.menu-icon {
display: flex;
.dropdown {
div[aria-expanded="false"] {
visibility: visible;
}
}
}
}
.menu-icon {
color: rgb(110 110 110);
}
&.item-focused-in-tab {
background: var(--color-sidebar-collection-item-active-background);
@ -56,6 +69,10 @@ const Wrapper = styled.div`
}
}
}
&.is-dragging .collection-item-name {
cursor: inherit;
}
`;
export default Wrapper;

View File

@ -1,5 +1,4 @@
import React, { useState, useRef, forwardRef } from 'react';
import get from 'lodash/get';
import range from 'lodash/range';
import classnames from 'classnames';
import { IconChevronRight, IconDots } from '@tabler/icons';
@ -19,6 +18,7 @@ import StyledWrapper from './StyledWrapper';
const CollectionItem = ({item, collection}) => {
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const isDragging = useSelector((state) => state.app.isDragging);
const dispatch = useDispatch();
const [renameItemModalOpen, setRenameItemModalOpen] = useState(false);
@ -44,15 +44,6 @@ const CollectionItem = ({item, collection}) => {
});
const handleClick = (event) => {
let tippyEl = get(dropdownTippyRef, 'current.reference');
if(tippyEl && tippyEl.contains && tippyEl.contains(event.target)) {
return;
}
if(event && event.target && event.target.className === 'dropdown-item') {
return;
}
if(isItemARequest(item)) {
if(itemIsOpenedInTabs(item, tabs)) {
dispatch(focusTab({
@ -76,24 +67,27 @@ const CollectionItem = ({item, collection}) => {
const onDropdownCreate = (ref) => dropdownTippyRef.current = ref;
const isFolder = isItemAFolder(item);
const className = classnames('flex flex-col w-full', {
'is-dragging': isDragging
});
return (
<StyledWrapper className="flex flex-col">
<StyledWrapper className={className}>
{renameItemModalOpen && <RenameCollectionItem item={item} collection={collection} onClose={() => setRenameItemModalOpen(false)}/>}
{deleteItemModalOpen && <DeleteCollectionItem item={item} collection={collection} onClose={() => setDeleteItemModalOpen(false)}/>}
{newRequestModalOpen && <NewRequest item={item} collection={collection} onClose={() => setNewRequestModalOpen(false)}/>}
{newFolderModalOpen && <NewFolder item={item} collection={collection} onClose={() => setNewFolderModalOpen(false)}/>}
<div
className={itemRowClassName}
onClick={handleClick}
>
<div className={itemRowClassName}>
<div className="flex items-center h-full w-full">
{indents && indents.length ? indents.map((i) => {
return (
<div
onClick={handleClick}
className="indent-block"
key={i}
style = {{
width: 16,
minWidth: 16,
height: '100%'
}}
>
@ -102,20 +96,21 @@ const CollectionItem = ({item, collection}) => {
);
}) : null}
<div
className="flex items-center"
onClick={handleClick}
className="flex flex-grow items-center h-full overflow-hidden"
style = {{
paddingLeft: 8
}}
>
<div style={{width:16}}>
<div style={{width:16, minWidth: 16}}>
{isFolder ? (
<IconChevronRight size={16} strokeWidth={2} className={iconClassName} style={{color: 'rgb(160 160 160)'}}/>
) : null}
</div>
<div className="ml-1 flex items-center">
<div className="ml-1 flex items-center overflow-hidden">
<RequestMethod item={item}/>
<div>{item.name}</div>
<span className="item-name" title={item.name}>{item.name}</span>
</div>
</div>
<div className="menu-icon pr-2">

View File

@ -6,7 +6,6 @@ const Wrapper = styled.div`
cursor: pointer;
user-select: none;
padding-left: 8px;
padding-right: 8px;
font-weight: 600;
.rotate-90 {
@ -14,9 +13,15 @@ const Wrapper = styled.div`
}
.collection-actions {
display: none;
flex-grow: 1;
justify-content: flex-end;
.dropdown {
div[aria-expanded="true"] {
visibility: visible;
}
div[aria-expanded="false"] {
visibility: hidden;
}
}
svg {
height: 22px;
@ -26,7 +31,11 @@ const Wrapper = styled.div`
&:hover {
.collection-actions {
display: flex;
.dropdown {
div[aria-expanded="false"] {
visibility: visible;
}
}
}
}

View File

@ -1,9 +1,8 @@
import React, { useState, forwardRef, useRef } from 'react';
import get from 'lodash/get';
import classnames from 'classnames';
import { IconChevronRight, IconDots } from '@tabler/icons';
import Dropdown from 'components/Dropdown';
import { collectionClicked } from 'providers/ReduxStore/slices/collections';
import { collectionClicked, removeCollection } from 'providers/ReduxStore/slices/collections';
import { useDispatch } from 'react-redux';
import NewRequest from 'components/Sidebar/NewRequest';
import NewFolder from 'components/Sidebar/NewFolder';
@ -20,26 +19,17 @@ const Collection = ({collection}) => {
const onMenuDropdownCreate = (ref) => menuDropdownTippyRef.current = ref;
const MenuIcon = forwardRef((props, ref) => {
return (
<div ref={ref}>
<div ref={ref} className="pr-2">
<IconDots size={22}/>
</div>
);
});
const iconClassName = classnames({
'rotate-90': collection.collapsed
'rotate-90': !collection.collapsed
});
const handleClick = (event) => {
let tippyEl = get(menuDropdownTippyRef, 'current.reference');
if(tippyEl && tippyEl.contains && tippyEl.contains(event.target)) {
return;
}
if(event && event.target && event.target.className === 'dropdown-item') {
return;
}
dispatch(collectionClicked(collection.uid));
};
@ -47,11 +37,17 @@ const Collection = ({collection}) => {
<StyledWrapper className="flex flex-col">
{showNewRequestModal && <NewRequest collection={collection} onClose={() => setShowNewRequestModal(false)}/>}
{showNewFolderModal && <NewFolder collection={collection} onClose={() => setShowNewFolderModal(false)}/>}
<div className="flex py-1 collection-name items-center" onClick={handleClick}>
<IconChevronRight size={16} strokeWidth={2} className={iconClassName} style={{width:16, color: 'rgb(160 160 160)'}}/>
<span className="ml-1">{collection.name}</span>
<div className="collection-actions">
<Dropdown onCreate={onMenuDropdownCreate} icon={<MenuIcon />} placement='bottom-start'>
<div className="flex py-1 collection-name items-center">
<div className="flex flex-grow items-center" onClick={handleClick}>
<IconChevronRight size={16} strokeWidth={2} className={iconClassName} style={{width:16, color: 'rgb(160 160 160)'}}/>
<div className="ml-1">{collection.name}</div>
</div>
<div className='collection-actions'>
<Dropdown
onCreate={onMenuDropdownCreate}
icon={<MenuIcon />}
placement='bottom-start'
>
<div className="dropdown-item" onClick={(e) => {
menuDropdownTippyRef.current.hide();
setShowNewRequestModal(true)
@ -64,12 +60,18 @@ const Collection = ({collection}) => {
}}>
New Folder
</div>
<div className="dropdown-item" onClick={(e) => {
dispatch(removeCollection(collection.uid));
menuDropdownTippyRef.current.hide();
}}>
Remove
</div>
</Dropdown>
</div>
</div>
<div>
{collection.collapsed ? (
{!collection.collapsed ? (
<div>
{collection.items && collection.items.length ? collection.items.map((i) => {
return <CollectionItem

View File

@ -8,11 +8,12 @@ const CreateCollection = ({handleConfirm, handleCancel}) => {
const formik = useFormik({
enableReinitialize: true,
initialValues: {
collectionName: ''
collectionName: '',
collectionLocation: ''
},
validationSchema: Yup.object({
collectionName: Yup.string()
.min(3, 'must be atleast 3 characters')
.min(1, 'must be atleast 1 characters')
.max(50, 'must be 50 characters or less')
.required('name is required')
}),
@ -33,10 +34,11 @@ const CreateCollection = ({handleConfirm, handleCancel}) => {
<Modal
size="sm"
title='Create Collection'
confirmText='Create'
handleConfirm={onSubmit}
handleCancel={handleCancel}
>
<form className="grafnode-form" onSubmit={formik.handleSubmit}>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div>
<label htmlFor="collectionName" className="block font-semibold">Name</label>
<input
@ -46,6 +48,7 @@ const CreateCollection = ({handleConfirm, handleCancel}) => {
ref={inputRef}
className="block textbox mt-2 w-full"
onChange={formik.handleChange}
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={formik.values.collectionName || ''}
/>
{formik.touched.collectionName && formik.errors.collectionName ? (

View File

@ -41,13 +41,14 @@ const NewFolder = ({collection, item, onClose}) => {
handleConfirm={onSubmit}
handleCancel={onClose}
>
<form className="grafnode-form" onSubmit={formik.handleSubmit}>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div>
<label htmlFor="folderName" className="block font-semibold">Folder Name</label>
<input
id="collection-name" type="text" name="folderName"
ref={inputRef}
className="block textbox mt-2 w-full"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
onChange={formik.handleChange}
value={formik.values.folderName || ''}
/>

View File

@ -0,0 +1,37 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.method-selector-container {
border: solid 1px var(--color-layout-border);
border-right: none;
background-color: var(--color-sidebar-background);
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
.method-selector {
min-width: 80px;
}
}
div.method-selector-container, div.input-container {
height: 2.3rem;
}
div.input-container {
border: solid 1px var(--color-layout-border);
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
input {
outline: none;
box-shadow: none;
&:focus {
outline: none !important;
box-shadow: none !important;
}
}
}
`;
export default StyledWrapper;

View File

@ -1,18 +1,24 @@
import React, { useRef, useEffect } from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import { uuid } from 'utils/common';;
import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
import { newHttpRequest } from 'providers/ReduxStore/slices/collections';
import { newHttpRequest, newEphermalHttpRequest } from 'providers/ReduxStore/slices/collections';
import { addTab } from 'providers/ReduxStore/slices/tabs';
import HttpMethodSelector from 'components/RequestPane/QueryUrl/HttpMethodSelector';
import StyledWrapper from './StyledWrapper';
const NewRequest = ({collection, item, onClose}) => {
const NewRequest = ({collection, item, isEphermal, onClose}) => {
const dispatch = useDispatch();
const inputRef = useRef();
const formik = useFormik({
enableReinitialize: true,
initialValues: {
requestName: ''
requestName: '',
requestType: 'http-request',
requestUrl: '',
requestMethod: 'get'
},
validationSchema: Yup.object({
requestName: Yup.string()
@ -21,13 +27,36 @@ const NewRequest = ({collection, item, onClose}) => {
.required('name is required')
}),
onSubmit: (values) => {
dispatch(newHttpRequest(values.requestName, collection.uid, item ? item.uid : null))
.then((action) => {
dispatch(addTab({
uid: action.payload.item.uid,
collectionUid: collection.uid
}));
});
if(isEphermal) {
const uid = uuid();
dispatch(newEphermalHttpRequest({
uid: uid,
requestName: values.requestName,
requestType: values.requestType,
requestUrl: values.requestUrl,
requestMethod: values.requestMethod,
collectionUid: collection.uid
}))
dispatch(addTab({
uid: uid,
collectionUid: collection.uid
}));
} else {
dispatch(newHttpRequest({
requestName: values.requestName,
requestType: values.requestType,
requestUrl: values.requestUrl,
requestMethod: values.requestMethod,
collectionUid: collection.uid,
itemUid: item ? item.uid : null
}))
.then((action) => {
dispatch(addTab({
uid: action.payload.item.uid,
collectionUid: collection.uid
}));
});
}
onClose();
}
});
@ -41,29 +70,83 @@ const NewRequest = ({collection, item, onClose}) => {
const onSubmit = () => formik.handleSubmit();
return (
<Modal
size="sm"
title='New Request'
confirmText='Create'
handleConfirm={onSubmit}
handleCancel={onClose}
>
<form className="grafnode-form" onSubmit={formik.handleSubmit}>
<div>
<label htmlFor="requestName" className="block font-semibold">Request Name</label>
<input
id="collection-name" type="text" name="requestName"
ref={inputRef}
className="block textbox mt-2 w-full"
onChange={formik.handleChange}
value={formik.values.requestName || ''}
/>
{formik.touched.requestName && formik.errors.requestName ? (
<div className="text-red-500">{formik.errors.requestName}</div>
) : null}
</div>
</form>
</Modal>
<StyledWrapper>
<Modal
size="md"
title='New Request'
confirmText='Create'
handleConfirm={onSubmit}
handleCancel={onClose}
>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div className="hidden">
<label htmlFor="requestName" className="block font-semibold">Type</label>
<div className="flex items-center mt-2">
<input
id="http-request"
className="cursor-pointer"
type="radio" name="requestType"
onChange={formik.handleChange}
value="http-request"
checked={formik.values.requestType === 'http-request'}
/>
<label htmlFor="http-request" className="ml-1 cursor-pointer select-none">Http</label>
<input
id="graphql-request"
className="ml-4 cursor-pointer"
type="radio" name="requestType"
onChange={(event) => {
formik.setFieldValue('requestMethod', 'post')
formik.handleChange(event);
}}
value="graphql-request"
checked={formik.values.requestType === 'graphql-request'}
/>
<label htmlFor="graphql-request" className="ml-1 cursor-pointer select-none">Graphql</label>
</div>
</div>
<div>
<label htmlFor="requestName" className="block font-semibold">Name</label>
<input
id="collection-name" type="text" name="requestName"
ref={inputRef}
className="block textbox mt-2 w-full"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
onChange={formik.handleChange}
value={formik.values.requestName || ''}
/>
{formik.touched.requestName && formik.errors.requestName ? (
<div className="text-red-500">{formik.errors.requestName}</div>
) : null}
</div>
<div className="mt-4">
<label htmlFor="request-url" className="block font-semibold">Url</label>
<div className="flex items-center mt-2 ">
<div className="flex items-center h-full method-selector-container">
<HttpMethodSelector method={formik.values.requestMethod} onMethodSelect={(val) => formik.setFieldValue('requestMethod', val)}/>
</div>
<div className="flex items-center flex-grow input-container h-full">
<input
id="request-url" type="text" name="requestUrl"
className="px-3 w-full "
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
onChange={formik.handleChange}
value={formik.values.requestUrl || ''}
/>
</div>
</div>
{formik.touched.requestUrl && formik.errors.requestUrl ? (
<div className="text-red-500">{formik.errors.requestUrl}</div>
) : null}
</div>
</form>
</Modal>
</StyledWrapper>
);
};

View File

@ -1,27 +1,46 @@
import styled from 'styled-components';
const Wrapper = styled.aside`
background-color: var(--color-sidebar-background);
const Wrapper = styled.div`
aside {
background-color: var(--color-sidebar-background);
.collection-title {
line-height: 1.5;
.collection-dropdown {
.dropdown-icon {
display: none;
color: rgb(110 110 110);
.collection-title {
line-height: 1.5;
.collection-dropdown {
.dropdown-icon {
display: none;
color: rgb(110 110 110);
}
}
&:hover {
background: #f7f7f7;
.dropdown-icon {
display: flex;
}
}
div.tippy-box {
position: relative;
top: -0.625rem;
}
}
}
&:hover {
background: #f7f7f7;
.dropdown-icon {
display: flex;
}
}
div.drag-sidebar {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
cursor: col-resize;
background-color: transparent;
width: 6px;
right: -3px;
div.tippy-box {
position: relative;
top: -0.625rem;
&:hover div.drag-request-border{
width: 2px;
height: 100%;
border-left: solid 1px var(--color-request-dragbar-background-active);
}
}
`;

View File

@ -14,7 +14,6 @@ const StyledWrapper = styled.div`
user-select: none;
}
}
`;
export default StyledWrapper;

View File

@ -27,11 +27,11 @@ const TitleBar = () => {
const handleConfirm = (values) => {
setModalOpen(false);
dispatch(createCollection(values.collectionName))
dispatch(createCollection(values.collectionName, values.collectionLocation));
};
return (
<StyledWrapper className="px-2 py-2 flex items-center">
<StyledWrapper className="px-2 py-2">
{showToast.show && <Toast text={showToast.text} type={showToast.type} duration={showToast.duration} handleClose={handleCloseToast}/>}
{modalOpen ? (
<CreateCollection
@ -40,28 +40,47 @@ const TitleBar = () => {
/>
) : null}
<div>
<span className="ml-2">Collections</span>
</div>
<div className="collection-dropdown flex flex-grow items-center justify-end">
<Dropdown onCreate={onMenuDropdownCreate} icon={<MenuIcon />} placement='bottom-start'>
<div className="dropdown-item" onClick={(e) => {
menuDropdownTippyRef.current.hide();
setModalOpen(true);
}}>
Create Collection
</div>
<div className="dropdown-item" onClick={(e) => {
menuDropdownTippyRef.current.hide();
}}>
Import Collection
</div>
<div className="dropdown-item" onClick={(e) => {
menuDropdownTippyRef.current.hide();
}}>
Settings
</div>
</Dropdown>
<div className="flex items-center">
<div className="flex items-center">
<svg id="emoji" width="30" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="color">
<path fill="#F4AA41" stroke="none" d="M23.5,14.5855l-4.5,1.75l-7.25,8.5l-4.5,10.75l2,5.25c1.2554,3.7911,3.5231,7.1832,7.25,10l2.5-3.3333 c0,0,3.8218,7.7098,10.7384,8.9598c0,0,10.2616,1.936,15.5949-0.8765c3.4203-1.8037,4.4167-4.4167,4.4167-4.4167l3.4167-3.4167 l1.5833,2.3333l2.0833-0.0833l5.4167-7.25L64,37.3355l-0.1667-4.5l-2.3333-5.5l-4.8333-7.4167c0,0-2.6667-4.9167-8.1667-3.9167 c0,0-6.5-4.8333-11.8333-4.0833S32.0833,10.6688,23.5,14.5855z"/>
<polygon fill="#EA5A47" stroke="none" points="36,47.2521 32.9167,49.6688 30.4167,49.6688 30.3333,53.5021 31.0833,57.0021 32.1667,58.9188 35,60.4188 39.5833,59.8355 41.1667,58.0855 42.1667,53.8355 41.9167,49.8355 39.9167,50.0855"/>
<polygon fill="#3F3F3F" stroke="none" points="32.5,36.9188 30.9167,40.6688 33.0833,41.9188 34.3333,42.4188 38.6667,42.5855 41.5833,40.3355 39.8333,37.0855"/>
</g>
<g id="hair"/>
<g id="skin"/>
<g id="skin-shadow"/>
<g id="line">
<path fill="#000000" stroke="none" d="M29.5059,30.1088c0,0-1.8051,1.2424-2.7484,0.6679c-0.9434-0.5745-1.2424-1.8051-0.6679-2.7484 s1.805-1.2424,2.7484-0.6679S29.5059,30.1088,29.5059,30.1088z"/>
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M33.1089,37.006h6.1457c0.4011,0,0.7634,0.2397,0.9203,0.6089l1.1579,2.7245l-2.1792,1.1456 c-0.6156,0.3236-1.3654-0.0645-1.4567-0.754"/>
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M34.7606,40.763c-0.1132,0.6268-0.7757,0.9895-1.3647,0.7471l-2.3132-0.952l1.0899-2.9035 c0.1465-0.3901,0.5195-0.6486,0.9362-0.6486"/>
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M30.4364,50.0268c0,0-0.7187,8.7934,3.0072,9.9375c2.6459,0.8125,5.1497,0.5324,6.0625-0.25 c0.875-0.75,2.6323-4.4741,1.8267-9.6875"/>
<path fill="#000000" stroke="none" d="M44.2636,30.1088c0,0,1.805,1.2424,2.7484,0.6679c0.9434-0.5745,1.2424-1.8051,0.6679-2.7484 c-0.5745-0.9434-1.805-1.2424-2.7484-0.6679C43.9881,27.9349,44.2636,30.1088,44.2636,30.1088z"/>
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M25.6245,42.8393c-0.475,3.6024,2.2343,5.7505,4.2847,6.8414c1.1968,0.6367,2.6508,0.5182,3.7176-0.3181l2.581-2.0233l2.581,2.0233 c1.0669,0.8363,2.5209,0.9548,3.7176,0.3181c2.0504-1.0909,4.7597-3.239,4.2847-6.8414"/>
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M19.9509,28.3572c-2.3166,5.1597-0.5084,13.0249,0.119,15.3759c0.122,0.4571,0.0755,0.9355-0.1271,1.3631l-1.9874,4.1937 c-0.623,1.3146-2.3934,1.5533-3.331,0.4409c-3.1921-3.7871-8.5584-11.3899-6.5486-16.686 c7.0625-18.6104,15.8677-18.1429,15.8677-18.1429c2.8453-1.9336,13.1042-6.9375,24.8125,0.875c0,0,8.6323-1.7175,14.9375,16.9375 c1.8036,5.3362-3.4297,12.8668-6.5506,16.6442c-0.9312,1.127-2.7162,0.8939-3.3423-0.4272l-1.9741-4.1656 c-0.2026-0.4275-0.2491-0.906-0.1271-1.3631c0.6275-2.3509,2.4356-10.2161,0.119-15.3759"/>
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M52.6309,46.4628c0,0-3.0781,6.7216-7.8049,8.2712"/>
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M19.437,46.969c0,0,3.0781,6.0823,7.8049,7.632"/>
<line x1="36.2078" x2="36.2078" y1="47.3393" y2="44.3093" fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2"/>
</g>
</svg>
</div>
<div className=" flex items-center font-medium select-none" style={{fontSize: 14, paddingLeft: 6, position: 'relative', top: -1}}>bruno</div>
<div className="collection-dropdown flex flex-grow items-center justify-end">
<Dropdown onCreate={onMenuDropdownCreate} icon={<MenuIcon />} placement='bottom-start'>
<div className="dropdown-item" onClick={(e) => {
menuDropdownTippyRef.current.hide();
}}>
Open Collection
</div>
<div className="dropdown-item" onClick={(e) => {
menuDropdownTippyRef.current.hide();
setModalOpen(true);
}}>
Create Collection
</div>
</Dropdown>
</div>
</div>
</StyledWrapper>
)

View File

@ -1,54 +1,111 @@
import React from 'react';
import React, { useState, useEffect} from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { toggleLeftMenuBar } from 'providers/ReduxStore/slices/app'
import Collections from './Collections';
import MenuBar from './MenuBar';
import TitleBar from './TitleBar';
import { IconSearch, IconChevronsRight} from '@tabler/icons';
import MenuBar from './MenuBar';
import { IconSearch, IconChevronsRight, IconSettings, IconShieldCheck, IconShieldX, IconLayoutGrid} from '@tabler/icons';
import { updateLeftSidebarWidth, updateIsDragging, toggleLeftMenuBar } from 'providers/ReduxStore/slices/app';
import StyledWrapper from './StyledWrapper';
const MIN_LEFT_SIDEBAR_WIDTH = 222;
const MAX_LEFT_SIDEBAR_WIDTH = 600;
const Sidebar = () => {
const leftMenuBarOpen = useSelector((state) => state.app.leftMenuBarOpen);
const leftSidebarWidth = useSelector((state) => state.app.leftSidebarWidth);
const leftMenuBarOpen = useSelector((state) => state.app.leftMenuBarOpen);
const [asideWidth, setAsideWidth] = useState(leftSidebarWidth);
const dispatch = useDispatch();
const [dragging, setDragging] = useState(false);
const handleMouseMove = (e) => {
if(dragging) {
e.preventDefault();
let width = e.clientX + 2;
if(width < MIN_LEFT_SIDEBAR_WIDTH || width > MAX_LEFT_SIDEBAR_WIDTH) {
return;
}
setAsideWidth(width);
}
};
const handleMouseUp = (e) => {
if(dragging) {
e.preventDefault();
setDragging(false);
dispatch(updateLeftSidebarWidth({
leftSidebarWidth: asideWidth
}));
dispatch(updateIsDragging({
isDragging: false
}));
}
};
const handleDragbarMouseDown = (e) => {
e.preventDefault();
setDragging(true);
dispatch(updateIsDragging({
isDragging: true
}));
};
useEffect(() => {
document.addEventListener('mouseup', handleMouseUp);
document.addEventListener('mousemove', handleMouseMove);
return () => {
document.removeEventListener('mouseup', handleMouseUp);
document.removeEventListener('mousemove', handleMouseMove);
};
}, [dragging, asideWidth]);
useEffect(() => {
setAsideWidth(leftSidebarWidth);
}, [leftSidebarWidth]);
return (
<StyledWrapper style={{width: `${leftSidebarWidth}px`, minWidth: `${leftSidebarWidth}px`}}>
<div className="flex flex-row h-full">
{leftMenuBarOpen && <MenuBar />}
<StyledWrapper className="flex relative">
<aside style={{width: `${asideWidth}px`, minWidth: `${asideWidth}px`}}>
<div className="flex flex-row h-full w-full">
{leftMenuBarOpen && <MenuBar />}
<div className="flex flex-col flex-grow">
<div className="flex flex-col flex-grow">
<TitleBar />
<div className="flex flex-col w-full">
<div className="flex flex-col flex-grow">
<TitleBar />
<div className="mt-4 relative collection-filter px-2">
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<span className="text-gray-500 sm:text-sm">
<IconSearch size={16} strokeWidth={1.5}/>
</span>
<div className="mt-4 relative collection-filter px-2">
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<span className="text-gray-500 sm:text-sm">
<IconSearch size={16} strokeWidth={1.5}/>
</span>
</div>
<input
type="text"
name="search"
id="search"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
className="block w-full pl-7 py-1 sm:text-sm"
placeholder="search"
/>
</div>
<input
type="text"
name="price"
id="price"
className="block w-full pl-7 py-1 sm:text-sm"
placeholder="search"
/>
</div>
<Collections />
<Collections />
</div>
<div className="flex px-1 py-2 items-center cursor-pointer text-gray-500 select-none">
<div className="flex items-center ml-1 text-xs ">
{!leftMenuBarOpen && <IconChevronsRight size={24} strokeWidth={1.5} className="mr-2 hover:text-gray-700" onClick={() => dispatch(toggleLeftMenuBar())}/>}
{/* <IconLayoutGrid size={20} strokeWidth={1.5} className="mr-2"/> */}
<IconSettings size={20} strokeWidth={1.5} className="mr-2 hover:text-gray-700"/>
</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">
v1.25.2
</div>
</div>
</div>
{/*
Sidebar is deprecated as we shift to the approach for storing collections as a file tree
on the filesystem itself
*/}
{/* <div
onClick={() => dispatch(toggleLeftMenuBar())}
className="flex flex-col px-1 py-2 cursor-pointer text-gray-500 hover:text-gray-700"
>
{!leftMenuBarOpen && <IconChevronsRight size={24} strokeWidth={1.5}/>}
</div> */}
</div>
</aside>
<div className="absolute drag-sidebar h-full" onMouseDown={handleDragbarMouseDown}>
<div className="drag-request-border" />
</div>
</StyledWrapper>
);

View File

@ -1,7 +1,7 @@
import styled from 'styled-components';
const Wrapper = styled.div`
&.grafnode-toast {
&.bruno-toast {
position: fixed;
top: 0;
left: 0;
@ -11,7 +11,7 @@ const Wrapper = styled.div`
justify-content: center;
}
.grafnode-toast-card {
.bruno-toast-card {
-webkit-animation-duration: .85s;
animation-duration: .85s;
-webkit-animation-delay: .1s;

View File

@ -27,8 +27,8 @@ const Toast = ({
}, [text]);
return (
<StyledWrapper className='grafnode-toast'>
<div className='grafnode-toast-card'>
<StyledWrapper className='bruno-toast'>
<div className='bruno-toast-card'>
<ToastContent type={type} text={text} handleClose={handleClose}></ToastContent>
</div>
</StyledWrapper>

View File

@ -6,17 +6,16 @@ const StyledWrapper = styled.div`
font-size: 0.75rem;
}
.create-request-options {
.http, .graphql {
cursor: pointer;
padding-right: 1rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
.collection-options {
svg {
position: relative;
top: -1px;
}
.label {
cursor: pointer;
&:hover {
span.name {
text-decoration: underline;
}
text-decoration: underline;
}
}
}

View File

@ -1,83 +1,47 @@
import React from 'react';
import { IconPlus, IconUpload } from '@tabler/icons';
import React, { useState } from 'react';
import { IconPlus, IconUpload, IconFolders } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { createCollection } from 'providers/ReduxStore/slices/collections';
import Bruno from 'components/Bruno';
import CreateCollection from 'components/Sidebar/CreateCollection';
import StyledWrapper from './StyledWrapper';
const Welcome = () => {
const newGraphqlRequest = () => {
// todo
};
const newHttpRequest = () => {
// todo
const [modalOpen, setModalOpen] = useState(false);
const dispatch = useDispatch();
const handleCancel = () => setModalOpen(false);
const handleConfirm = (values) => {
setModalOpen(false);
dispatch(createCollection(values.collectionName, values.collectionLocation));
};
return (
<StyledWrapper className="pb-4 px-6 mt-6">
<div className="text-3xl">
<svg id="emoji" width="50" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="color">
<path fill="#F4AA41" stroke="none" d="M23.5,14.5855l-4.5,1.75l-7.25,8.5l-4.5,10.75l2,5.25c1.2554,3.7911,3.5231,7.1832,7.25,10l2.5-3.3333 c0,0,3.8218,7.7098,10.7384,8.9598c0,0,10.2616,1.936,15.5949-0.8765c3.4203-1.8037,4.4167-4.4167,4.4167-4.4167l3.4167-3.4167 l1.5833,2.3333l2.0833-0.0833l5.4167-7.25L64,37.3355l-0.1667-4.5l-2.3333-5.5l-4.8333-7.4167c0,0-2.6667-4.9167-8.1667-3.9167 c0,0-6.5-4.8333-11.8333-4.0833S32.0833,10.6688,23.5,14.5855z"/>
<polygon fill="#EA5A47" stroke="none" points="36,47.2521 32.9167,49.6688 30.4167,49.6688 30.3333,53.5021 31.0833,57.0021 32.1667,58.9188 35,60.4188 39.5833,59.8355 41.1667,58.0855 42.1667,53.8355 41.9167,49.8355 39.9167,50.0855"/>
<polygon fill="#3F3F3F" stroke="none" points="32.5,36.9188 30.9167,40.6688 33.0833,41.9188 34.3333,42.4188 38.6667,42.5855 41.5833,40.3355 39.8333,37.0855"/>
</g>
<g id="hair"/>
<g id="skin"/>
<g id="skin-shadow"/>
<g id="line">
<path fill="#000000" stroke="none" d="M29.5059,30.1088c0,0-1.8051,1.2424-2.7484,0.6679c-0.9434-0.5745-1.2424-1.8051-0.6679-2.7484 s1.805-1.2424,2.7484-0.6679S29.5059,30.1088,29.5059,30.1088z"/>
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M33.1089,37.006h6.1457c0.4011,0,0.7634,0.2397,0.9203,0.6089l1.1579,2.7245l-2.1792,1.1456 c-0.6156,0.3236-1.3654-0.0645-1.4567-0.754"/>
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M34.7606,40.763c-0.1132,0.6268-0.7757,0.9895-1.3647,0.7471l-2.3132-0.952l1.0899-2.9035 c0.1465-0.3901,0.5195-0.6486,0.9362-0.6486"/>
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M30.4364,50.0268c0,0-0.7187,8.7934,3.0072,9.9375c2.6459,0.8125,5.1497,0.5324,6.0625-0.25 c0.875-0.75,2.6323-4.4741,1.8267-9.6875"/>
<path fill="#000000" stroke="none" d="M44.2636,30.1088c0,0,1.805,1.2424,2.7484,0.6679c0.9434-0.5745,1.2424-1.8051,0.6679-2.7484 c-0.5745-0.9434-1.805-1.2424-2.7484-0.6679C43.9881,27.9349,44.2636,30.1088,44.2636,30.1088z"/>
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M25.6245,42.8393c-0.475,3.6024,2.2343,5.7505,4.2847,6.8414c1.1968,0.6367,2.6508,0.5182,3.7176-0.3181l2.581-2.0233l2.581,2.0233 c1.0669,0.8363,2.5209,0.9548,3.7176,0.3181c2.0504-1.0909,4.7597-3.239,4.2847-6.8414"/>
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M19.9509,28.3572c-2.3166,5.1597-0.5084,13.0249,0.119,15.3759c0.122,0.4571,0.0755,0.9355-0.1271,1.3631l-1.9874,4.1937 c-0.623,1.3146-2.3934,1.5533-3.331,0.4409c-3.1921-3.7871-8.5584-11.3899-6.5486-16.686 c7.0625-18.6104,15.8677-18.1429,15.8677-18.1429c2.8453-1.9336,13.1042-6.9375,24.8125,0.875c0,0,8.6323-1.7175,14.9375,16.9375 c1.8036,5.3362-3.4297,12.8668-6.5506,16.6442c-0.9312,1.127-2.7162,0.8939-3.3423-0.4272l-1.9741-4.1656 c-0.2026-0.4275-0.2491-0.906-0.1271-1.3631c0.6275-2.3509,2.4356-10.2161,0.119-15.3759"/>
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M52.6309,46.4628c0,0-3.0781,6.7216-7.8049,8.2712"/>
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M19.437,46.969c0,0,3.0781,6.0823,7.8049,7.632"/>
<line x1="36.2078" x2="36.2078" y1="47.3393" y2="44.3093" fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2"/>
</g>
</svg>
{modalOpen ? (
<CreateCollection
handleCancel={handleCancel}
handleConfirm={handleConfirm}
/>
) : null}
<div className="">
<Bruno width={50} />
</div>
<div className="text-xl font-semibold">grafnode</div>
<div className="mt-1">Opensource API collection collaboration platform</div>
<div className="text-xl font-semibold select-none">bruno</div>
<div className="mt-4">Opensource API Client.</div>
<div className="uppercase font-semibold create-request mt-8">Create Request</div>
<div className="flex mt-4 create-request-options">
<div className="flex items-center mr-2 http" onClick={newHttpRequest}>
<span style={{color: '#1662c3'}}>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" className="bi bi-globe" viewBox="0 0 16 16">
<path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm7.5-6.923c-.67.204-1.335.82-1.887 1.855A7.97 7.97 0 0 0 5.145 4H7.5V1.077zM4.09 4a9.267 9.267 0 0 1 .64-1.539 6.7 6.7 0 0 1 .597-.933A7.025 7.025 0 0 0 2.255 4H4.09zm-.582 3.5c.03-.877.138-1.718.312-2.5H1.674a6.958 6.958 0 0 0-.656 2.5h2.49zM4.847 5a12.5 12.5 0 0 0-.338 2.5H7.5V5H4.847zM8.5 5v2.5h2.99a12.495 12.495 0 0 0-.337-2.5H8.5zM4.51 8.5a12.5 12.5 0 0 0 .337 2.5H7.5V8.5H4.51zm3.99 0V11h2.653c.187-.765.306-1.608.338-2.5H8.5zM5.145 12c.138.386.295.744.468 1.068.552 1.035 1.218 1.65 1.887 1.855V12H5.145zm.182 2.472a6.696 6.696 0 0 1-.597-.933A9.268 9.268 0 0 1 4.09 12H2.255a7.024 7.024 0 0 0 3.072 2.472zM3.82 11a13.652 13.652 0 0 1-.312-2.5h-2.49c.062.89.291 1.733.656 2.5H3.82zm6.853 3.472A7.024 7.024 0 0 0 13.745 12H11.91a9.27 9.27 0 0 1-.64 1.539 6.688 6.688 0 0 1-.597.933zM8.5 12v2.923c.67-.204 1.335-.82 1.887-1.855.173-.324.33-.682.468-1.068H8.5zm3.68-1h2.146c.365-.767.594-1.61.656-2.5h-2.49a13.65 13.65 0 0 1-.312 2.5zm2.802-3.5a6.959 6.959 0 0 0-.656-2.5H12.18c.174.782.282 1.623.312 2.5h2.49zM11.27 2.461c.247.464.462.98.64 1.539h1.835a7.024 7.024 0 0 0-3.072-2.472c.218.284.418.598.597.933zM10.855 4a7.966 7.966 0 0 0-.468-1.068C9.835 1.897 9.17 1.282 8.5 1.077V4h2.355z"/>
</svg>
</span>
<span className="ml-2 name">Http</span>
</div>
<div className="flex items-center graphql" onClick={newGraphqlRequest}>
<svg
xmlns="http://www.w3.org/2000/svg"
height="26"
width="26"
viewBox="0 0 29.999 30"
fill="#e10098"
>
<path d="M4.08 22.864l-1.1-.636L15.248.98l1.1.636z"/>
<path d="M2.727 20.53h24.538v1.272H2.727z"/>
<path d="M15.486 28.332L3.213 21.246l.636-1.1 12.273 7.086zm10.662-18.47L13.874 2.777l.636-1.1 12.273 7.086z"/>
<path d="M3.852 9.858l-.636-1.1L15.5 1.67l.636 1.1z"/>
<path d="M25.922 22.864l-12.27-21.25 1.1-.636 12.27 21.25zM3.7 7.914h1.272v14.172H3.7zm21.328 0H26.3v14.172h-1.272z"/>
<path d="M15.27 27.793l-.555-.962 10.675-6.163.555.962z"/>
<path d="M27.985 22.5a2.68 2.68 0 0 1-3.654.981 2.68 2.68 0 0 1-.981-3.654 2.68 2.68 0 0 1 3.654-.981c1.287.743 1.724 2.375.98 3.654M6.642 10.174a2.68 2.68 0 0 1-3.654.981A2.68 2.68 0 0 1 2.007 7.5a2.68 2.68 0 0 1 3.654-.981 2.68 2.68 0 0 1 .981 3.654M2.015 22.5a2.68 2.68 0 0 1 .981-3.654 2.68 2.68 0 0 1 3.654.981 2.68 2.68 0 0 1-.981 3.654c-1.287.735-2.92.3-3.654-.98m21.343-12.326a2.68 2.68 0 0 1 .981-3.654 2.68 2.68 0 0 1 3.654.981 2.68 2.68 0 0 1-.981 3.654 2.68 2.68 0 0 1-3.654-.981M15 30a2.674 2.674 0 1 1 2.674-2.673A2.68 2.68 0 0 1 15 30m0-24.652a2.67 2.67 0 0 1-2.674-2.674 2.67 2.67 0 1 1 5.347 0A2.67 2.67 0 0 1 15 5.347"/>
</svg>
<span className="ml-2 name">GraphQL</span>
</div>
</div>
<div className="uppercase font-semibold create-request mt-8">Collections</div>
<div className="mt-4">
<div className="uppercase font-semibold create-request mt-10">Collections</div>
<div className="mt-4 flex items-center collection-options">
<div className="flex items-center">
<IconPlus size={18} strokeWidth={2}/><span className="ml-2">Create Collection</span>
<IconFolders size={18} strokeWidth={2}/><span className="label ml-2">Open Collection</span>
</div>
<div className="flex items-center mt-2">
<IconUpload size={18} strokeWidth={2}/><span className="ml-2">Import Collection</span>
<div className="flex items-center ml-6">
<IconPlus size={18} strokeWidth={2}/><span className="label ml-2" onClick={() => setModalOpen(true)}>Create Collection</span>
</div>
{/* not in mvp */}
{/* <div className="flex items-center ml-6">
<IconUpload size={18} strokeWidth={2}/><span className="label ml-2">Import Collection</span>
</div> */}
</div>
</StyledWrapper>

View File

@ -6,6 +6,10 @@ const Wrapper = styled.div`
height: 100%;
min-height: 100vh;
&.is-dragging {
cursor: col-resize !important;
}
section.main {
display: flex;

View File

@ -1,4 +1,5 @@
import React from 'react';
import classnames from 'classnames';
import RequestTabs from 'components/RequestTabs';
import RequestTabPanel from 'components/RequestTabPanel';
import Sidebar from 'components/Sidebar';
@ -28,17 +29,17 @@ if(!SERVER_RENDERED) {
require('codemirror-graphql/mode');
}
export default function Main() {
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const isDragging = useSelector((state) => state.app.isDragging);
if (SERVER_RENDERED) {
return null;
}
const className = classnames({
'is-dragging': isDragging
});
return (
<div>
<StyledWrapper>
<StyledWrapper className={className}>
<Sidebar />
<section className='flex flex-grow flex-col'>
<RequestTabs />

View File

@ -85,8 +85,8 @@ const Login = () => {
</g>
</svg>
</div>
<div className="font-semibold" style={{fontSize: '2rem'}}>grafnode</div>
<div className="mt-1">Opensource API Collection Collaboration</div>
<div className="font-semibold" style={{fontSize: '2rem'}}>bruno</div>
<div className="mt-1">Opensource API Collection Collaboration Platform</div>
</div>
<form onSubmit={formik.handleSubmit}>
<div className="flex justify-center flex-col form-container mx-auto mt-10 p-5">

View File

@ -96,8 +96,8 @@ const SignUp = () => {
</g>
</svg>
</div>
<div className="font-semibold" style={{fontSize: '2rem'}}>grafnode</div>
<div className="mt-1">Opensource API Collection Collaboration</div>
<div className="font-semibold" style={{fontSize: '2rem'}}>bruno</div>
<div className="mt-1">Opensource API Collection Collaboration Platform</div>
</div>
<form onSubmit={formik.handleSubmit}>

View File

@ -20,18 +20,34 @@ function SafeHydrate({ children }) {
)
}
function NoSsr({ children }) {
const SERVER_RENDERED = typeof navigator === 'undefined';
if(SERVER_RENDERED) {
return null;
}
return (
<>
{children}
</>
)
}
function MyApp({ Component, pageProps }) {
return (
<SafeHydrate>
<AuthProvider>
<Provider store={ReduxStore}>
<AppProvider>
<HotkeysProvider>
<Component {...pageProps} />
</HotkeysProvider>
</AppProvider>
</Provider>
</AuthProvider>
<NoSsr>
<AuthProvider>
<Provider store={ReduxStore}>
<AppProvider>
<HotkeysProvider>
<Component {...pageProps} />
</HotkeysProvider>
</AppProvider>
</Provider>
</AuthProvider>
</NoSsr>
</SafeHydrate>
);
}

View File

@ -1,19 +1,19 @@
import Head from 'next/head';
import Main from 'pageComponents/Main';
import IndexPage from 'pageComponents/Index';
import GlobalStyle from '../globalStyles';
export default function Home() {
return (
<div>
<Head>
<title>grafnode</title>
<title>bruno</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<GlobalStyle />
<main>
<Main />
<IndexPage />
</main>
</div>
);

View File

@ -7,7 +7,7 @@ export default function LoginPage() {
return (
<div>
<Head>
<title>grafnode</title>
<title>bruno</title>
<link rel="icon" href="/favicon.ico" />
</Head>

View File

@ -7,7 +7,7 @@ export default function SignUpPage() {
return (
<div>
<Head>
<title>grafnode</title>
<title>bruno</title>
<link rel="icon" href="/favicon.ico" />
</Head>

View File

@ -1,5 +1,7 @@
import React from 'react';
import React, { useEffect } from 'react';
import useIdb from './useIdb';
import { useDispatch } from 'react-redux';
import { refreshScreenWidth } from 'providers/ReduxStore/slices/app';
export const AppContext = React.createContext();
@ -7,6 +9,22 @@ export const AppProvider = props => {
// boot idb
useIdb();
const dispatch = useDispatch();
useEffect(() => {
dispatch(refreshScreenWidth());
}, []);
useEffect(() => {
const handleResize = () => {
dispatch(refreshScreenWidth());
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return (
<AppContext.Provider {...props} value='appProvider'>
{props.children}

View File

@ -1,9 +1,9 @@
import React, { useEffect } from 'react';
import React, { useState, useEffect } from 'react';
import find from 'lodash/find';
import Mousetrap from 'mousetrap';
import { useSelector, useDispatch } from 'react-redux';
import SaveRequest from 'components/RequestPane/SaveRequest';
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections';
import { requestSaved } from 'providers/ReduxStore/slices/tabs';
import { findCollectionByUid, findItemInCollection } from 'utils/collections';
export const HotkeysContext = React.createContext();
@ -13,18 +13,30 @@ export const HotkeysProvider = props => {
const tabs = useSelector((state) => state.tabs.tabs);
const collections = useSelector((state) => state.collections.collections);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const [showSaveRequestModal, setShowSaveRequestModal] = useState(false);
const getCurrentCollectionItems = () => {
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
if(activeTab) {
const collection = findCollectionByUid(collections, activeTab.collectionUid);
return collection ? collection.items : [];
};
};
// save hotkey
useEffect(() => {
Mousetrap.bind(['command+s', 'ctrl+s'], (e) => {
if(activeTabUid) {
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
if(activeTab) {
// todo: these dispatches need to be chained and errors need to be handled
dispatch(saveRequest(activeTab.uid, activeTab.collectionUid));
dispatch(requestSaved({
itemUid: activeTab.uid
}))
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
if(activeTab) {
const collection = findCollectionByUid(collections, activeTab.collectionUid);
if(collection) {
const item = findItemInCollection(collection, activeTab.uid);
if(item && item.uid) {
dispatch(saveRequest(activeTab.uid, activeTab.collectionUid))
} else {
setShowSaveRequestModal(true);
}
}
}
@ -34,21 +46,19 @@ export const HotkeysProvider = props => {
return () => {
Mousetrap.unbind(['command+s', 'ctrl+s']);
};
}, [activeTabUid, tabs, saveRequest, requestSaved]);
}, [activeTabUid, tabs, saveRequest, collections]);
// send request (ctrl/cmd + enter)
useEffect(() => {
Mousetrap.bind(['ctrl+command', 'ctrl+enter'], (e) => {
if(activeTabUid) {
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
if(activeTab) {
const collection = findCollectionByUid(collections, activeTab.collectionUid);
Mousetrap.bind(['command+enter', 'ctrl+enter'], (e) => {
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
if(activeTab) {
const collection = findCollectionByUid(collections, activeTab.collectionUid);
if(collection) {
const item = findItemInCollection(collection, activeTab.uid);
if(item) {
dispatch(sendRequest(item, collection.uid));
}
if(collection) {
const item = findItemInCollection(collection, activeTab.uid);
if(item) {
dispatch(sendRequest(item, collection.uid));
}
}
}
@ -57,13 +67,16 @@ export const HotkeysProvider = props => {
});
return () => {
Mousetrap.unbind(['ctrl+command', 'ctrl+enter']);
Mousetrap.unbind(['command+enter', 'ctrl+enter']);
};
}, [activeTabUid, tabs, saveRequest, requestSaved, collections]);
}, [activeTabUid, tabs, saveRequest, collections]);
return (
<HotkeysContext.Provider {...props} value='hotkey'>
{props.children}
{showSaveRequestModal && <SaveRequest items={getCurrentCollectionItems()} onClose={() => setShowSaveRequestModal(false)}/>}
<div>
{props.children}
</div>
</HotkeysContext.Provider>
);
};

View File

@ -1,25 +1,42 @@
import { createSlice } from '@reduxjs/toolkit'
const initialState = {
isDragging: false,
idbConnectionReady: false,
leftMenuBarOpen: false,
leftSidebarWidth: 222
leftSidebarWidth: 270,
leftMenuBarOpen: true,
screenWidth: 500
};
export const appSlice = createSlice({
name: 'app',
initialState,
reducers: {
idbConnectionReady: (state) => {
state.idbConnectionReady = true;
},
toggleLeftMenuBar: (state) => {
state.leftMenuBarOpen = !state.leftMenuBarOpen;
state.leftSidebarWidth = state.leftMenuBarOpen ? 270 : 222;
},
idbConnectionReady: (state) => {
state.idbConnectionReady = true;
}
refreshScreenWidth: (state) => {
state.screenWidth = window.innerWidth;
},
updateLeftSidebarWidth: (state, action) => {
state.leftSidebarWidth = action.payload.leftSidebarWidth;
},
updateIsDragging: (state, action) => {
state.isDragging = action.payload.isDragging;
},
}
});
export const { toggleLeftMenuBar, idbConnectionReady } = appSlice.actions;
export const {
idbConnectionReady,
toggleLeftMenuBar,
refreshScreenWidth,
updateLeftSidebarWidth,
updateIsDragging
} = appSlice.actions;
export default appSlice.reducer;

View File

@ -1,10 +1,12 @@
import { nanoid } from 'nanoid';
import path from 'path';
import { uuid } from 'utils/common';
import trim from 'lodash/trim';
import find from 'lodash/find';
import concat from 'lodash/concat';
import filter from 'lodash/filter';
import each from 'lodash/each';
import cloneDeep from 'lodash/cloneDeep';
import { createSlice } from '@reduxjs/toolkit'
import { getCollectionsFromIdb, saveCollectionToIdb } from 'utils/idb';
import splitOnFirst from 'split-on-first';
import { sendNetworkRequest } from 'utils/network';
import {
findCollectionByUid,
@ -13,8 +15,11 @@ import {
transformCollectionToSaveToIdb,
addDepth,
deleteItemInCollection,
isItemARequest
isItemARequest,
} from 'utils/collections';
import { parseQueryParams, stringifyQueryParams } from 'utils/url';
import { getCollectionsFromIdb, saveCollectionToIdb } from 'utils/idb';
import { each } from 'lodash';
// todo: errors should be tracked in each slice and displayed as toasts
@ -22,6 +27,8 @@ const initialState = {
collections: []
};
const PATH_SEPARATOR = path.sep;
export const collectionsSlice = createSlice({
name: 'collections',
initialState,
@ -33,39 +40,6 @@ export const collectionsSlice = createSlice({
_createCollection: (state, action) => {
state.collections.push(action.payload);
},
_requestSent: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if(collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if(item) {
item.response = item.response || {};
item.response.state = 'sending';
}
}
},
_responseReceived: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if(collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if(item) {
item.response = action.payload.response;
}
}
},
_saveRequest: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if(collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if(item && item.draft) {
item.request = item.draft.request;
item.draft = null;
}
}
},
_newItem: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
@ -101,6 +75,63 @@ export const collectionsSlice = createSlice({
}
}
},
_requestSent: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if(collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if(item) {
item.response = item.response || {};
item.response.state = 'sending';
}
}
},
_responseReceived: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if(collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if(item) {
item.response = action.payload.response;
}
}
},
_saveRequest: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if(collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if(item && item.draft) {
item.request = item.draft.request;
item.draft = null;
}
}
},
newEphermalHttpRequest: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if(collection && collection.items && collection.items.length) {
const item = {
uid: action.payload.uid,
name: action.payload.requestName,
type: action.payload.requestType,
request: {
url: action.payload.requestUrl,
method: action.payload.requestMethod,
params: [],
headers: [],
body: {
mode: null,
content: null
}
},
draft: null
};
item.draft = cloneItem(item);
collection.items.push(item);
}
},
collectionClicked: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload);
@ -130,6 +161,113 @@ export const collectionsSlice = createSlice({
item.draft = cloneItem(item);
}
item.draft.request.url = action.payload.url;
const parts = splitOnFirst(item.draft.request.url, '?');
const urlParams = parseQueryParams(parts[1]);
const disabledParams = filter(item.draft.request.params, (p) => !p.enabled);
let enabledParams = filter(item.draft.request.params, (p) => p.enabled);
// try and connect as much as old params uid's as possible
each(urlParams, (urlParam) => {
const existingParam = find(enabledParams, (p) => p.name === urlParam.name || p.value === urlParam.value);
urlParam.uid = existingParam ? existingParam.uid : uuid();
urlParam.enabled = true;
// once found, remove it - trying our best here to accomodate duplicate query params
if(existingParam) {
enabledParams = filter(enabledParams, (p) => p.uid !== existingParam.uid);
}
});
// ultimately params get replaced with params in url + the disabled onces that existed prior
// the query params are the source of truth, the url in the queryurl input gets constructed using these params
// we however are also storing the full url (with params) in the url itself
item.draft.request.params = concat(urlParams, disabledParams);
}
}
},
addQueryParam: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if(collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if(item && isItemARequest(item)) {
if(!item.draft) {
item.draft = cloneItem(item);
}
item.draft.request.params = item.draft.request.params || [];
item.draft.request.params.push({
uid: uuid(),
name: '',
value: '',
description: '',
enabled: true
});
}
}
},
updateQueryParam: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if(collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if(item && isItemARequest(item)) {
if(!item.draft) {
item.draft = cloneItem(item);
}
const param = find(item.draft.request.params, (h) => h.uid === action.payload.param.uid);
if(param) {
param.name = action.payload.param.name;
param.value = action.payload.param.value;
param.description = action.payload.param.description;
param.enabled = action.payload.param.enabled;
// update request url
const parts = splitOnFirst(item.draft.request.url, '?');
const query = stringifyQueryParams(filter(item.draft.request.params, p => p.enabled));
// if no query is found, then strip the query params in url
if(!query || !query.length) {
if(parts.length) {
item.draft.request.url = parts[0];
}
return;
}
// if no parts were found, then append the query
if(!parts.length) {
item.draft.request.url += '?' + query;
return;
}
// control reaching here means the request has parts and query is present
item.draft.request.url = parts[0] + '?' + query;
}
}
}
},
deleteQueryParam: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if(collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if(item && isItemARequest(item)) {
if(!item.draft) {
item.draft = cloneItem(item);
}
item.draft.request.params = filter(item.draft.request.params, (p) => p.uid !== action.payload.paramUid);
// update request url
const parts = splitOnFirst(item.draft.request.url, '?');
const query = stringifyQueryParams(filter(item.draft.request.params, p => p.enabled));
if(query && query.length) {
item.draft.request.url = parts[0] + '?' + query;
} else {
item.draft.request.url = parts[0];
}
}
}
},
@ -145,7 +283,7 @@ export const collectionsSlice = createSlice({
}
item.draft.request.headers = item.draft.request.headers || [];
item.draft.request.headers.push({
uid: nanoid(),
uid: uuid(),
name: '',
value: '',
description: '',
@ -204,29 +342,49 @@ export const collectionsSlice = createSlice({
}
}
}
},
updateRequestMethod: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if(collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if(item && isItemARequest(item)) {
if(!item.draft) {
item.draft = cloneItem(item);
}
item.draft.request.method = action.payload.method;
}
}
}
}
});
export const {
_loadCollections,
_createCollection,
_requestSent,
_responseReceived,
_saveRequest,
_loadCollections,
_newItem,
_deleteItem,
_renameItem,
_requestSent,
_responseReceived,
_saveRequest,
newEphermalHttpRequest,
collectionClicked,
collectionFolderClicked,
requestUrlChanged,
addQueryParam,
updateQueryParam,
deleteQueryParam,
addRequestHeader,
updateRequestHeader,
deleteRequestHeader,
updateRequestBody
updateRequestBody,
updateRequestMethod
} = collectionsSlice.actions;
export const loadCollectionsFromIdb = () => (dispatch) => {
console.log('here');
getCollectionsFromIdb(window.__idb)
.then((collections) => dispatch(_loadCollections({
collections: collections
@ -236,11 +394,10 @@ export const loadCollectionsFromIdb = () => (dispatch) => {
export const createCollection = (collectionName) => (dispatch) => {
const newCollection = {
uid: nanoid(),
uid: uuid(),
name: collectionName,
items: [],
environments: [],
userId: null
};
saveCollectionToIdb(window.__idb, newCollection)
@ -266,19 +423,23 @@ export const saveRequest = (itemUid, collectionUid) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
if(collection) {
const collectionCopy = cloneDeep(collection);
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
saveCollectionToIdb(window.__idb, collectionToSave)
.then(() => {
dispatch(_saveRequest({
itemUid: itemUid,
collectionUid: collectionUid
}));
})
.catch((err) => console.log(err));
}
return new Promise((resolve, reject) => {
if(collection) {
const collectionCopy = cloneDeep(collection);
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
saveCollectionToIdb(window.__idb, collectionToSave)
.then(() => {
dispatch(_saveRequest({
itemUid: itemUid,
collectionUid: collectionUid
}));
})
.then(() => resolve())
.catch((error) => reject(error));
}
});
};
export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getState) => {
@ -288,7 +449,7 @@ export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getS
if(collection) {
const collectionCopy = cloneDeep(collection);
const item = {
uid: nanoid(),
uid: uuid(),
name: folderName,
type: 'folder',
items: []
@ -316,23 +477,33 @@ export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getS
}
};
export const newHttpRequest = (requestName, collectionUid, itemUid) => (dispatch, getState) => {
export const newHttpRequest = (params) => (dispatch, getState) => {
const {
requestName,
requestType,
requestUrl,
requestMethod,
collectionUid,
itemUid
} = params;
return new Promise((resolve, reject) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
if(collection) {
const collectionCopy = cloneDeep(collection);
const uid = nanoid();
const item = {
uid: uid,
uid: uuid(),
type: requestType,
name: requestName,
type: 'http-request',
request: {
method: 'GET',
url: 'https://reqbin.com/echo/get/json',
method: requestMethod,
url: requestUrl,
headers: [],
body: null
body: {
mode: 'none',
content: ''
}
}
};
if(!itemUid) {
@ -368,20 +539,23 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
if(collection) {
const collectionCopy = cloneDeep(collection);
deleteItemInCollection(itemUid, collectionCopy);
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
return new Promise((resolve, reject) => {
if(collection) {
const collectionCopy = cloneDeep(collection);
deleteItemInCollection(itemUid, collectionCopy);
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
saveCollectionToIdb(window.__idb, collectionToSave)
.then(() => {
dispatch(_deleteItem({
itemUid: itemUid,
collectionUid: collectionUid
}));
})
.catch((err) => console.log(err));
}
saveCollectionToIdb(window.__idb, collectionToSave)
.then(() => {
dispatch(_deleteItem({
itemUid: itemUid,
collectionUid: collectionUid
}));
})
.then(() => resolve())
.catch((error) => reject(error));
}
});
};
export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getState) => {
@ -410,4 +584,8 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta
}
};
export const removeCollection = (collectionPath) => () => {
console.log('removeCollection');
};
export default collectionsSlice.reducer;

View File

@ -7,8 +7,7 @@ import { createSlice } from '@reduxjs/toolkit'
const initialState = {
tabs: [],
activeTabUid: null,
hasChanges: false
activeTabUid: null
};
export const tabsSlice = createSlice({
@ -18,15 +17,40 @@ export const tabsSlice = createSlice({
addTab: (state, action) => {
state.tabs.push({
uid: action.payload.uid,
collectionUid: action.payload.collectionUid
collectionUid: action.payload.collectionUid,
requestPaneWidth: null,
requestPaneTab: 'params',
responsePaneTab: 'response'
});
state.activeTabUid = action.payload.uid;
},
focusTab: (state, action) => {
state.activeTabUid = action.payload.uid;
},
closeTab: (state, action) => {
state.tabs = filter(state.tabs, (t) => t.uid !== action.payload.tabUid);
updateRequestPaneTabWidth: (state, action) => {
const tab = find(state.tabs, (t) => t.uid === action.payload.uid);
if(tab) {
tab.requestPaneWidth = action.payload.requestPaneWidth;
}
},
updateRequestPaneTab: (state, action) => {
const tab = find(state.tabs, (t) => t.uid === action.payload.uid);
if(tab) {
tab.requestPaneTab = action.payload.requestPaneTab;
}
},
updateResponsePaneTab: (state, action) => {
const tab = find(state.tabs, (t) => t.uid === action.payload.uid);
if(tab) {
tab.responsePaneTab = action.payload.responsePaneTab;
}
},
closeTabs: (state, action) => {
const tabUids = action.payload.tabUids || [];
state.tabs = filter(state.tabs, (t) => !tabUids.includes(t.uid));
if(state.tabs && state.tabs.length) {
// todo: closing tab needs to focus on the right adjacent tab
@ -35,27 +59,44 @@ export const tabsSlice = createSlice({
state.activeTabUid = null;
}
},
requestChanged: (state, action) => {
const tab = find(state.tabs, (t) => t.uid == action.payload.itemUid);
if(tab) {
tab.hasChanges = true;
// todo: implement this
// the refreshTabs us currently not beng used
// the goal is to have the main page listen to unlink events and
// remove tabs which are no longer valid
refreshTabs: (state, action) => {
// remove all tabs that we don't have itemUids in all loaded collections
const allItemUids = action.payload.allItemUids || [];
state.tabs = filter(state.tabs, (tab) => {
return allItemUids.includes(tab.uid);
});
// adjust the activeTabUid
const collectionUid = action.payload.activeCollectionUid;
const collectionTabs = filter(state.tabs, (t) => t.collectionUid === collectionUid);
if(!collectionTabs || !collectionTabs.length) {
state.activeTabUid = null;
return;
}
},
requestSaved: (state, action) => {
const tab = find(state.tabs, (t) => t.uid == action.payload.itemUid);
if(tab) {
tab.hasChanges = false;
const activeTabStillExists = find(state.tabs, (t) => t.uid === state.activeTabUid);
if(!activeTabStillExists) {
// todo: closing tab needs to focus on the right adjacent tab
state.activeTabUid = last(collectionTabs).uid;
}
},
}
}
});
export const {
addTab,
focusTab,
closeTab,
requestChanged,
requestSaved
updateRequestPaneTabWidth,
updateRequestPaneTab,
updateResponsePaneTab,
closeTabs,
refreshTabs
} = tabsSlice.actions;
export default tabsSlice.reducer;

View File

@ -1,14 +1,14 @@
const collection = {
"id": nanoid(),
"id": uuid(),
"name": "spacex",
"items": [
{
"id": nanoid(),
"id": uuid(),
"name": "Launches",
"depth": 1,
"items": [
{
"id": nanoid(),
"id": uuid(),
"depth": 2,
"name": "Capsules",
"request": {
@ -27,7 +27,7 @@ const collection = {
"response": null
},
{
"id": nanoid(),
"id": uuid(),
"depth": 2,
"name": "Missions",
"request": {
@ -51,16 +51,16 @@ const collection = {
};
const collection2 = {
"id": nanoid(),
"id": uuid(),
"name": "notebase",
"items": [
{
"id": nanoid(),
"id": uuid(),
"name": "Notes",
"depth": 1,
"items": [
{
"id": nanoid(),
"id": uuid(),
"depth": 2,
"name": "Create",
"request": {
@ -79,7 +79,7 @@ const collection2 = {
"response": null
},
{
"id": nanoid(),
"id": uuid(),
"depth": 2,
"name": "Update",
"request": {

View File

@ -1,5 +1,4 @@
import produce from 'immer';
import {nanoid} from 'nanoid';
import union from 'lodash/union';
import find from 'lodash/find';
import actions from './actions';
@ -36,7 +35,7 @@ const reducer = (state, action) => {
case actions.ADD_NEW_GQL_REQUEST: {
return produce(state, (draft) => {
const uid = nanoid();
const uid = uuid();
draft.requestTabs.push({
uid: uid,
name: 'New Tab',

View File

@ -4,9 +4,10 @@
--color-sidebar-collection-item-active-indent-border: #d0d0d0;
--color-sidebar-collection-item-active-background: #dddddd;
--color-sidebar-background: #f3f3f3;
--color-request-dragbar-background: #e2e2e2;
--color-request-dragbar-background-active: #bbb;
--color-tab-active-border: #4d4d4d;
--color-request-dragbar-background: #f3f3f3;
--color-request-dragbar-background-active: rgb(200, 200, 200);
--color-tab-inactive: rgb(155 155 155);
--color-tab-active-border: #546de5;
--color-layout-border: #dedede;
--color-codemirror-border: #efefef;
--color-codemirror-background: rgb(243, 243, 243);
@ -31,6 +32,7 @@ html, body {
text-rendering: optimizeSpeed;
letter-spacing: normal;
font-family: Inter, sans-serif !important;
overflow-x: hidden;
}
body {
@ -49,3 +51,7 @@ body::-webkit-scrollbar-thumb, .CodeMirror-vscrollbar::-webkit-scrollbar-thumb {
background-color: #cdcdcd;
border-radius: 5rem;
}
.text-link {
color: var(--color-text-link);
}

View File

@ -1,9 +1,19 @@
import each from 'lodash/each';
import find from 'lodash/find';
import isString from 'lodash/isString';
import map from 'lodash/map';
import filter from 'lodash/filter';
import sortBy from 'lodash/sortBy';
import cloneDeep from 'lodash/cloneDeep';
const replaceTabsWithSpaces = (str, numSpaces = 2) => {
if(!str || !str.length || !isString(str)) {
return '';
}
return str.replaceAll('\t', ' '.repeat(numSpaces));
};
export const addDepth = (items = []) => {
const depth = (itms, initialDepth) => {
each(itms, (i) => {
@ -18,6 +28,18 @@ export const addDepth = (items = []) => {
depth(items, 1);
};
export const sortItems = (collection) => {
const sort = (obj) => {
if(obj.items && obj.items.length) {
obj.items = sortBy(obj.items, 'type');
}
each(obj.items, (i) => sort(i));
}
sort(collection);
};
export const flattenItems = (items = []) => {
const flattenedItems = [];
@ -40,6 +62,7 @@ export const findItem = (items = [], itemUid) => {
return find(items, (i) => i.uid === itemUid);
};
export const findCollectionByUid = (collections, collectionUid) => {
return find(collections, (c) => c.uid === collectionUid);
};
@ -50,6 +73,12 @@ export const findItemInCollection = (collection, itemUid) => {
return findItem(flattenedItems, itemUid);
}
export const recursivelyGetAllItemUids = (items = []) => {
let flattenedItems = flattenItems(items);
return map(flattenedItems, (i) => i.uid);
};
export const cloneItem = (item) => {
return cloneDeep(item);
};
@ -83,7 +112,10 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
url: si.draft.request.url,
method: si.draft.request.method,
headers: copyHeaders(si.draft.request.headers),
body: si.draft.request.body
body: {
mode: si.draft.request.body.mode,
content: replaceTabsWithSpaces(si.draft.request.body.content)
}
};
}
} else {
@ -92,11 +124,18 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
url: si.request.url,
method: si.request.method,
headers: copyHeaders(si.request.headers),
body: si.request.body
body: {
mode: si.request.body.mode,
content: replaceTabsWithSpaces(si.request.body.content)
}
}
};
}
if(di.request && di.request.body.mode === 'json') {
di.request.body.content = replaceTabsWithSpaces(di.request.body.content);
}
destItems.push(di);
if(si.items && si.items.length) {
@ -110,6 +149,7 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
collectionToSave.name = collection.name;
collectionToSave.uid = collection.uid;
collectionToSave.userId = collection.userId;
collectionToSave.orgId = collection.orgId;
collectionToSave.environments = cloneDeep(collection.environments);
collectionToSave.items = [];

View File

@ -0,0 +1,20 @@
import { customAlphabet } from 'nanoid';
// a customized version of nanoid without using _ and -
export const uuid = () => {
// https://github.com/ai/nanoid/blob/main/url-alphabet/index.js
const urlAlphabet = 'useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict';
const customNanoId = customAlphabet (urlAlphabet, 21);
return customNanoId();
};
export const simpleHash = str => {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash &= hash; // Convert to 32bit integer
}
return new Uint32Array([hash])[0].toString(36);
};

View File

@ -12,7 +12,7 @@ const sendNetworkRequest = async (item) => {
state: 'success',
data: response.data,
headers: Object.entries(response.headers),
size: response.headers["content-length"],
size: response.headers["content-length"] || 0,
status: response.status,
duration: timeEnd - timeStart
});
@ -24,9 +24,8 @@ const sendNetworkRequest = async (item) => {
const sendHttpRequest = async (request) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window.require("electron");
const { ipcRenderer } = window;
console.log(request);
const headers = {};
each(request.headers, (h) => {
if(h.enabled) {
@ -41,8 +40,9 @@ const sendHttpRequest = async (request) => {
};
if(request.body && request.body.mode === 'json' && request.body.content) {
options.data = request.body.content;
options.data = JSON.parse(request.body.content);
}
console.log(request);
ipcRenderer
.invoke('send-http-request', options)

View File

@ -0,0 +1,38 @@
import isEmpty from 'lodash/isEmpty';
import trim from 'lodash/trim';
import each from 'lodash/each';
import splitOnFirst from 'split-on-first';
export const parseQueryParams = (query) => {
if(!query || !query.length) {
return [];
}
let params = query.split("&");
let result = [];
for (let i = 0; i < params.length; i++) {
let pair = splitOnFirst(params[i], '=');;
result.push({
name: pair[0],
value: pair[1]
});
}
return result;
};
export const stringifyQueryParams = (params) => {
if(!params || isEmpty(params)) {
return '';
}
let queryString = [];
each(params, (p) => {
if(!isEmpty(trim(p.name)) && !isEmpty(trim(p.value))) {
queryString.push(`${p.name}=${p.value}`);
}
});
return queryString.join('&');
};