mirror of
https://github.com/usebruno/bruno.git
synced 2024-11-25 01:14:23 +01:00
feat: September 2022 updates
This commit is contained in:
parent
81b88e964f
commit
c7cced5868
@ -1 +0,0 @@
|
||||
{"type": "commonjs"}
|
22
LICENSE
22
LICENSE
@ -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.
|
@ -1,6 +1 @@
|
||||
## development
|
||||
|
||||
```bash
|
||||
# run this at root
|
||||
lerna bootstrap --hoist
|
||||
```
|
10
lerna.json
10
lerna.json
@ -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
55
main/app/menu-template.js
Normal 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;
|
@ -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
33
main/ipc.js
Normal 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;
|
@ -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
14
main/utils/common.js
Normal 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
12580
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
|
17
readme.md
17
readme.md
@ -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
|
||||
|
@ -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;
|
30
renderer/components/Bruno/index.js
Normal file
30
renderer/components/Bruno/index.js
Normal 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;
|
@ -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");
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
@ -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>
|
||||
)
|
||||
};
|
||||
|
@ -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 {
|
||||
|
@ -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"] {
|
||||
|
@ -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}>
|
||||
+ <span>Add Param</span>
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
)
|
||||
};
|
||||
|
@ -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;
|
@ -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;
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
29
renderer/components/RequestTabPanel/RequestNotFound/index.js
Normal file
29
renderer/components/RequestTabPanel/RequestNotFound/index.js
Normal 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;
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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 />
|
||||
|
38
renderer/components/RequestTabs/RequestTab/StyledWrapper.js
Normal file
38
renderer/components/RequestTabs/RequestTab/StyledWrapper.js
Normal 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;
|
||||
|
78
renderer/components/RequestTabs/RequestTab/index.js
Normal file
78
renderer/components/RequestTabs/RequestTab/index.js
Normal 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;
|
@ -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;
|
||||
|
@ -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>
|
||||
</>
|
||||
|
@ -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);
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -13,7 +13,7 @@ const ResponseSize = ({size}) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-3 ml-4">
|
||||
<StyledWrapper className="ml-4">
|
||||
{sizeToDisplay}
|
||||
</StyledWrapper>
|
||||
)
|
||||
|
@ -13,7 +13,7 @@ const ResponseTime = ({duration}) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-3 ml-4">
|
||||
<StyledWrapper className="ml-4">
|
||||
{durationToDisplay}
|
||||
</StyledWrapper>
|
||||
)
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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();
|
||||
};
|
||||
|
||||
|
@ -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 || ''}
|
||||
/>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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;
|
||||
|
@ -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">
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 ? (
|
||||
|
@ -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 || ''}
|
||||
/>
|
||||
|
37
renderer/components/Sidebar/NewRequest/StyledWrapper.js
Normal file
37
renderer/components/Sidebar/NewRequest/StyledWrapper.js
Normal 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;
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -14,7 +14,6 @@ const StyledWrapper = styled.div`
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
@ -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>
|
||||
)
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -6,6 +6,10 @@ const Wrapper = styled.div`
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
|
||||
&.is-dragging {
|
||||
cursor: col-resize !important;
|
||||
}
|
||||
|
||||
section.main {
|
||||
display: flex;
|
||||
|
@ -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 />
|
@ -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">
|
||||
|
@ -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}>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -7,7 +7,7 @@ export default function LoginPage() {
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>grafnode</title>
|
||||
<title>bruno</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
||||
|
@ -7,7 +7,7 @@ export default function SignUpPage() {
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>grafnode</title>
|
||||
<title>bruno</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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": {
|
||||
|
@ -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',
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 = [];
|
||||
|
||||
|
20
renderer/utils/common/index.js
Normal file
20
renderer/utils/common/index.js
Normal 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);
|
||||
};
|
@ -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)
|
||||
|
38
renderer/utils/url/index.js
Normal file
38
renderer/utils/url/index.js
Normal 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('&');
|
||||
};
|
Loading…
Reference in New Issue
Block a user